diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html
index 8ec84c4..3b6432b 100644
--- a/docs/dvx_basic_reference.html
+++ b/docs/dvx_basic_reference.html
@@ -147,465 +147,465 @@ img { max-width: 100%; }
Index
diff --git a/docs/dvx_help_viewer.html b/docs/dvx_help_viewer.html
index 504c495..c3877f5 100644
--- a/docs/dvx_help_viewer.html
+++ b/docs/dvx_help_viewer.html
@@ -36,18 +36,18 @@ img { max-width: 100%; }
Index
diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html
index d8b72a9..2505119 100644
--- a/docs/dvx_system_reference.html
+++ b/docs/dvx_system_reference.html
@@ -171,709 +171,709 @@ img { max-width: 100%; }
Index
diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile
index db1ef88..ff62c7f 100644
--- a/src/apps/kpunch/Makefile
+++ b/src/apps/kpunch/Makefile
@@ -142,10 +142,15 @@ $(BINDIR)/kpunch/widshow: ; mkdir -p $@
# Header dependencies
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
+# Every widget public header. Apps that pull in widget APIs (e.g. dvxdemo)
+# must rebuild when these change, otherwise the struct-layout contract
+# between the widget DXE's sApi and the app's cast to RadioApiT/etc.
+# silently drifts and produces late-binding crashes.
+WIDGET_H = $(wildcard ../../widgets/kpunch/*/*.h)
$(OBJDIR)/cpanel.o: cpanel/cpanel.c $(COMMON_H) ../../libs/kpunch/libdvx/dvxPrefs.h ../../libs/kpunch/libdvx/platform/dvxPlat.h
$(OBJDIR)/progman.o: progman/progman.c $(COMMON_H) ../../libs/kpunch/dvxshell/shellInf.h
$(OBJDIR)/clock.o: clock/clock.c ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxDraw.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h ../../libs/kpunch/libtasks/taskSwch.h
-$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H)
+$(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c $(COMMON_H) $(WIDGET_H)
clean:
rm -f $(OBJDIR)/*.o
diff --git a/src/apps/kpunch/basdemo/basdemo.frm b/src/apps/kpunch/basdemo/basdemo.frm
index e36531f..2f0235d 100644
--- a/src/apps/kpunch/basdemo/basdemo.frm
+++ b/src/apps/kpunch/basdemo/basdemo.frm
@@ -162,7 +162,6 @@ End
' IN THE SOFTWARE.
-
OPTION EXPLICIT
TYPE PointT
@@ -185,22 +184,6 @@ dynCount = 0
timerWin = 0
tickCount = 0
-
-' ============================================================
-' OutArea helpers
-' ============================================================
-
-SUB Say(s AS STRING)
- OutArea.AppendText s + CHR$(10)
-END SUB
-
-
-SUB Header(title AS STRING)
- Say ""
- Say "--- " + title + " ---"
-END SUB
-
-
Load BasicDemo
BasicDemo.Show
@@ -212,9 +195,16 @@ Say "Check the Demos menu for graphics, dynamic UI, and timer demos."
Say ""
-' ============================================================
-' Menu handlers
-' ============================================================
+SUB Say(s AS STRING)
+ OutArea.AppendText s + CHR$(10)
+END SUB
+
+
+SUB Header(title AS STRING)
+ Say ""
+ Say "--- " + title + " ---"
+END SUB
+
SUB mnuClear_Click
OutArea.Text = ""
@@ -266,12 +256,8 @@ SUB mnuAbout_Click
MsgBox msg, vbOKOnly, "About"
END SUB
-
-' ============================================================
-' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
-' ============================================================
-
SUB btnTypes_Click
+ ' Types: INTEGER, LONG, SINGLE, DOUBLE, STRING, BOOLEAN
Header "Types"
DIM i AS INTEGER
@@ -302,12 +288,8 @@ SUB btnTypes_Click
LblStatus.Caption = "Types demo complete."
END SUB
-
-' ============================================================
-' Math: integer + float operators, built-in functions
-' ============================================================
-
SUB btnMath_Click
+ ' Math: integer + float operators, built-in functions
Header "Math"
DIM a AS INTEGER
@@ -342,12 +324,8 @@ SUB btnMath_Click
LblStatus.Caption = "Math demo complete."
END SUB
-
-' ============================================================
-' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
-' ============================================================
-
SUB btnStrings_Click
+ ' Strings: concatenation, LEFT$/RIGHT$/MID$/LEN/INSTR/UCASE$/LCASE$
Header "Strings"
DIM s AS STRING
@@ -371,12 +349,8 @@ SUB btnStrings_Click
LblStatus.Caption = "Strings demo complete."
END SUB
-
-' ============================================================
-' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
-' ============================================================
-
SUB btnArrays_Click
+ ' Arrays: 1D, 2D, LBOUND/UBOUND, REDIM PRESERVE
Header "Arrays"
' 1D array
@@ -430,12 +404,8 @@ SUB btnArrays_Click
LblStatus.Caption = "Arrays demo complete."
END SUB
-
-' ============================================================
-' DATA / READ / RESTORE
-' ============================================================
-
SUB btnData_Click
+ ' DATA / READ / RESTORE
Header "DATA / READ / RESTORE"
DATA "Red", 255, 0, 0
@@ -465,12 +435,8 @@ SUB btnData_Click
LblStatus.Caption = "DATA/READ demo complete."
END SUB
-
-' ============================================================
-' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
-' ============================================================
-
SUB btnFlow_Click
+ ' Control flow: IF, SELECT CASE, FOR, DO WHILE, GOSUB
Header "Control Flow"
' FOR with STEP
@@ -538,12 +504,8 @@ SUB btnFlow_Click
RETURN
END SUB
-
-' ============================================================
-' User-Defined Type
-' ============================================================
-
SUB btnUdt_Click
+ ' User-Defined Type
Header "User-Defined Type"
DIM p AS PointT
@@ -566,11 +528,6 @@ SUB btnUdt_Click
LblStatus.Caption = "UDT demo complete."
END SUB
-
-' ============================================================
-' Optional parameters (DVX extension)
-' ============================================================
-
FUNCTION Greet(who AS STRING, OPTIONAL greeting AS STRING) AS STRING
IF greeting = "" THEN
greeting = "Hello"
@@ -589,12 +546,8 @@ SUB btnOpt_Click
LblStatus.Caption = "Optional params demo complete."
END SUB
-
-' ============================================================
-' ON ERROR GOTO
-' ============================================================
-
SUB btnError_Click
+ ' ON ERROR GOTO
Header "ON ERROR GOTO"
ON ERROR GOTO handler
@@ -613,12 +566,8 @@ SUB btnError_Click
LblStatus.Caption = "Error handler ran successfully."
END SUB
-
-' ============================================================
-' PRINT USING / FORMAT$
-' ============================================================
-
SUB btnFormat_Click
+ ' PRINT USING / FORMAT$
Header "Formatting"
Say "FORMAT$(1234.5, '#,##0.00') = " + FORMAT$(1234.5, "#,##0.00")
@@ -629,12 +578,8 @@ SUB btnFormat_Click
LblStatus.Caption = "Format demo complete."
END SUB
-
-' ============================================================
-' File I/O
-' ============================================================
-
SUB btnFileIO_Click
+ ' File I/O
Header "File I/O"
DIM path AS STRING
@@ -664,12 +609,8 @@ SUB btnFileIO_Click
LblStatus.Caption = "File I/O demo complete."
END SUB
-
-' ============================================================
-' System: App object, environment, current directory
-' ============================================================
-
SUB btnSystem_Click
+ ' System: App object, environment, current directory
Header "System / App"
Say "App.Path = " + App.Path
@@ -683,12 +624,8 @@ SUB btnSystem_Click
LblStatus.Caption = "System demo complete."
END SUB
-
-' ============================================================
-' INI read/write
-' ============================================================
-
SUB btnIni_Click
+ ' INI read/write
Header "INI Read/Write"
DIM path AS STRING
@@ -710,11 +647,6 @@ SUB btnIni_Click
LblStatus.Caption = "INI demo complete."
END SUB
-
-' ============================================================
-' Dialog demos (spawns the real dialogs)
-' ============================================================
-
SUB btnDialogs_Click
Header "Dialogs"
@@ -750,11 +682,6 @@ SUB btnClear_Click
mnuClear_Click
END SUB
-
-' ============================================================
-' Graphics demo (opens a second form with Canvas)
-' ============================================================
-
SUB mnuGraphics_Click
IF gfxWin <> 0 THEN
EXIT SUB
@@ -766,7 +693,7 @@ SUB mnuGraphics_Click
gfxWin = frm
DIM cv AS LONG
- SET cv = CreateControl(frm, "Canvas", "GfxCanvas")
+ SET cv = CreateControl(frm, "PictureBox", "GfxCanvas")
GfxCanvas.Width = 340
GfxCanvas.Height = 260
GfxCanvas.Weight = 1
@@ -866,11 +793,6 @@ SUB GraphicsForm_Unload
gfxWin = 0
END SUB
-
-' ============================================================
-' Dynamic form demo
-' ============================================================
-
SUB mnuDynamic_Click
IF dynForm <> 0 THEN
EXIT SUB
@@ -922,11 +844,6 @@ SUB DynForm_Unload
dynCount = 0
END SUB
-
-' ============================================================
-' Timer demo
-' ============================================================
-
SUB mnuTimer_Click
IF timerWin <> 0 THEN
EXIT SUB
diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c
index c4d15a7..92bf13b 100644
--- a/src/apps/kpunch/cpanel/cpanel.c
+++ b/src/apps/kpunch/cpanel/cpanel.c
@@ -996,9 +996,14 @@ static void scanThemes(void) {
nameLen = (int32_t)sizeof(entry.name) - 1;
}
+ // entry.name is the extension-stripped label shown in the
+ // dropdown. entry.path must use the ORIGINAL filename (with
+ // extension) so dvxLoadTheme can open it -- previously we
+ // used entry.name here, which pointed at a path missing
+ // .THEME and silently failed.
memcpy(entry.name, names[i], nameLen);
entry.name[nameLen] = '\0';
- snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, entry.name);
+ snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, names[i]);
arrput(sThemeEntries, entry);
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c
index 568417d..cbf567d 100644
--- a/src/apps/kpunch/dvxbasic/compiler/parser.c
+++ b/src/apps/kpunch/dvxbasic/compiler/parser.c
@@ -202,6 +202,7 @@ static void parseKill(BasParserT *p);
static void parseLineInput(BasParserT *p);
static void parseMkDir(BasParserT *p);
static void parseModule(BasParserT *p);
+static void prescanSignatures(BasParserT *p);
static void parseMulExpr(BasParserT *p);
static void parseName(BasParserT *p);
static void parseNotExpr(BasParserT *p);
@@ -2911,6 +2912,12 @@ static void parseFunction(BasParserT *p) {
funcSym->paramOptional[i] = paramOptional[i];
}
+ // Record the owning form -- see parseSub for the rationale.
+ if (p->sym.inFormScope && p->sym.formScopeName[0]) {
+ strncpy(funcSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
+ funcSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
+ }
+
// Backpatch any forward-reference calls to this function
patchCallAddrs(p, funcSym);
@@ -3370,7 +3377,193 @@ static void parseMkDir(BasParserT *p) {
}
+// Walk the token stream from current position, find every
+// top-level SUB/FUNCTION declaration, extract the signature
+// (name, params, return type), and register it in the symbol
+// table. Does not emit code. Saves and restores lexer position
+// so the main parse pass starts from the same point. This gives
+// VB-style forward visibility: call sites that appear earlier in
+// the source than the SUB definition still resolve to the right
+// paramCount / types.
+static void prescanSignatures(BasParserT *p) {
+ BasLexerT savedLex = p->lex;
+ bool savedErr = p->hasError;
+ int32_t savedErrLn = p->errorLine;
+ char savedErrMsg[BAS_PARSE_ERR_SCRATCH];
+ snprintf(savedErrMsg, sizeof(savedErrMsg), "%s", p->error);
+
+ while (!check(p, TOK_EOF)) {
+ // "END SUB" / "END FUNCTION" consume the END and the following
+ // keyword as separate tokens; skip END so the next-iteration
+ // SUB/FUNCTION check doesn't misinterpret it as a declaration.
+ if (check(p, TOK_END)) {
+ advance(p);
+ continue;
+ }
+
+ bool isFn = check(p, TOK_FUNCTION);
+ bool isSub = check(p, TOK_SUB);
+
+ if (!isFn && !isSub) {
+ advance(p);
+ continue;
+ }
+
+ advance(p); // consume SUB / FUNCTION
+
+ if (!check(p, TOK_IDENT)) {
+ continue;
+ }
+
+ char name[BAS_MAX_TOKEN_LEN];
+ strncpy(name, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
+ name[BAS_MAX_TOKEN_LEN - 1] = '\0';
+ advance(p);
+
+ // Param list
+ int32_t paramCount = 0;
+ int32_t requiredCount = 0;
+ uint8_t paramTypes[BAS_MAX_PARAMS] = {0};
+ bool paramByVal[BAS_MAX_PARAMS] = {0};
+ bool paramOptional[BAS_MAX_PARAMS] = {0};
+
+ if (check(p, TOK_LPAREN)) {
+ advance(p);
+
+ while (!check(p, TOK_RPAREN) && !check(p, TOK_EOF) && !check(p, TOK_NEWLINE)) {
+ if (paramCount > 0) {
+ if (!check(p, TOK_COMMA)) {
+ break;
+ }
+ advance(p);
+ }
+
+ bool optional = false;
+
+ if (check(p, TOK_OPTIONAL)) {
+ optional = true;
+ advance(p);
+ }
+
+ bool byVal = false;
+
+ if (check(p, TOK_BYVAL)) {
+ byVal = true;
+ advance(p);
+ }
+
+ if (!check(p, TOK_IDENT)) {
+ break;
+ }
+
+ char paramName[BAS_MAX_TOKEN_LEN];
+ strncpy(paramName, p->lex.token.text, BAS_MAX_TOKEN_LEN - 1);
+ paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
+ advance(p);
+
+ uint8_t pdt = suffixToType(paramName);
+
+ if (check(p, TOK_AS)) {
+ advance(p);
+ // resolveTypeName sets hasError on miss; we clear
+ // at function end so one broken signature doesn't
+ // stop us from discovering the rest.
+ pdt = resolveTypeName(p);
+
+ if (p->hasError) {
+ break;
+ }
+ }
+
+ if (paramCount < BAS_MAX_PARAMS) {
+ paramTypes[paramCount] = pdt;
+ paramByVal[paramCount] = byVal;
+ paramOptional[paramCount] = optional;
+ }
+
+ if (!optional) {
+ requiredCount = paramCount + 1;
+ }
+
+ paramCount++;
+ }
+
+ if (check(p, TOK_RPAREN)) {
+ advance(p);
+ }
+ }
+
+ // FUNCTION return type (AS clause; or suffix on name)
+ uint8_t returnType = suffixToType(name);
+
+ if (isFn && check(p, TOK_AS)) {
+ advance(p);
+ returnType = resolveTypeName(p);
+ }
+
+ // Register / update the symbol. If a call site already
+ // created a forward-ref stub, update it in place.
+ BasSymbolT *sym = basSymTabFindGlobal(&p->sym, name);
+
+ if (sym == NULL) {
+ bool savedLocal = p->sym.inLocalScope;
+ p->sym.inLocalScope = false;
+ sym = basSymTabAdd(&p->sym, name,
+ isFn ? SYM_FUNCTION : SYM_SUB,
+ returnType);
+ p->sym.inLocalScope = savedLocal;
+ }
+
+ if (sym != NULL) {
+ sym->scope = SCOPE_GLOBAL;
+ sym->dataType = returnType;
+ sym->paramCount = paramCount;
+ sym->requiredParams = requiredCount;
+
+ for (int32_t i = 0; i < paramCount && i < BAS_MAX_PARAMS; i++) {
+ sym->paramTypes[i] = paramTypes[i];
+ sym->paramByVal[i] = paramByVal[i];
+ sym->paramOptional[i] = paramOptional[i];
+ }
+
+ // basSymTabAdd defaults isDefined=true; clear it so
+ // call sites that encounter this symbol before the real
+ // body is parsed register themselves as forward-refs
+ // (patchAddrs). The real parseSub/parseFunction pass
+ // sets isDefined=true and fills in codeAddr, at which
+ // point patchCallAddrs backpatches the forward refs.
+ sym->isDefined = false;
+ sym->codeAddr = 0;
+ }
+
+ // Best-effort: clear any scan error so we continue to the
+ // next declaration. The main parse pass will re-surface
+ // real errors with full location info.
+ if (p->hasError) {
+ p->hasError = false;
+ p->errorLine = 0;
+ p->error[0] = '\0';
+ }
+ }
+
+ p->lex = savedLex;
+ p->hasError = savedErr;
+ p->errorLine = savedErrLn;
+ snprintf(p->error, sizeof(p->error), "%s", savedErrMsg);
+}
+
+
static void parseModule(BasParserT *p) {
+ // VB semantics: all SUB/FUNCTION declarations are visible from
+ // anywhere in the module regardless of source order. Do a
+ // pre-scan that walks the token stream, extracts every
+ // SUB/FUNCTION signature, and registers it in the symbol table.
+ // Call sites encountered later (including module-level code
+ // that precedes the SUB definition in source) can then validate
+ // argument count / types against the real signature instead of
+ // falling back to a paramCount=0 placeholder.
+ prescanSignatures(p);
+
skipNewlines(p);
while (!p->hasError && !check(p, TOK_EOF)) {
@@ -5843,6 +6036,16 @@ static void parseSub(BasParserT *p) {
subSym->paramOptional[i] = paramOptional[i];
}
+ // Record the owning form so fireCtrlEvent can bind the SUB's
+ // form-scope variables at call time. Prescan adds SUB symbols
+ // before the BEGINFORM directive is consumed, so the formName
+ // wasn't populated by basSymTabAdd; set it here once we know we
+ // are inside a form scope.
+ if (p->sym.inFormScope && p->sym.formScopeName[0]) {
+ strncpy(subSym->formName, p->sym.formScopeName, BAS_MAX_SYMBOL_NAME - 1);
+ subSym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
+ }
+
// Backpatch any forward-reference calls to this sub
patchCallAddrs(p, subSym);
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
index 1018e66..973fea2 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
@@ -34,7 +34,14 @@
#include "dvxRes.h"
#include "dvxWm.h"
#include "shellApp.h"
-#include "box/box.h"
+// Widget headers below publish inline accessor macros over
+// dvx*Api() -> wgtGetApi(...) lookups, so they're lightweight
+// compile-time type-safety over a runtime-dynamic dispatch -- no
+// link-time dependency on the widget implementation. AnsiTerm,
+// DataCtrl, and DbGrid need direct typed calls because their C
+// APIs take callback function pointers, peer widget pointers, or
+// string-returning-from-string signatures that don't fit the
+// basic-callable iface-method dispatch.
#include "ansiTerm/ansiTerm.h"
#include "dataCtrl/dataCtrl.h"
#include "dbGrid/dbGrid.h"
@@ -60,6 +67,16 @@
// Module-level form runtime pointer for onFormClose callback
static BasFormRtT *sFormRt = NULL;
+// True while a basFormRtLoadFrm is in progress. The .frm parser calls
+// basFormRtLoadForm via onFormBegin to create the bare form before it
+// starts firing onCtrlBegin callbacks; if we let basFormRtLoadForm run
+// the form's init-code during that window the init fires against a
+// control list that is still empty (and then crashes with
+// "unknown control: cboSize" etc.). basFormRtLoadFrm sets this true
+// around frmParse so the nested LoadForm skips init; the outer
+// caller runs init (or the event loop) after parsing completes.
+static bool sLoadingFrm = false;
+
// ============================================================
// Module-scope declarations
@@ -177,6 +194,7 @@ typedef struct {
bool checked;
bool radioCheck;
bool enabled;
+ bool visible; // level==0: true = menu bar entry, false = popup-only
} BasFrmMenuItemT;
@@ -241,6 +259,7 @@ static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char
static BasFormT *resolveOwningForm(BasFormRtT *rt, const BasProcEntryT *proc);
int32_t basPromptSave(const char *title);
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
+static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name);
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
void CommClose(int32_t handle);
void CommDetach(int32_t handle);
@@ -592,7 +611,8 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
if (!ctrl) {
basFormRtRuntimeError(rt,
"Method call on unknown control",
- "Method: %s\nControl was not found on any loaded form.",
+ "Control: %s\nMethod: %s\nControl was not found on any loaded form.",
+ (rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?",
methodName ? methodName : "?");
return zeroValue();
}
@@ -765,6 +785,24 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
basStringUnref(s);
}
return zeroValue();
+
+ case WGT_SIG_STR_STR:
+ if (argc >= 2) {
+ BasStringT *a = basValFormatString(args[0]);
+ BasStringT *b = basValFormatString(args[1]);
+ ((void (*)(WidgetT *, const char *, const char *))m->fn)(w, a->data, b->data);
+ basStringUnref(a);
+ basStringUnref(b);
+ }
+ return zeroValue();
+
+ case WGT_SIG_STR_BOOL:
+ if (argc >= 2) {
+ BasStringT *s = basValFormatString(args[0]);
+ ((void (*)(WidgetT *, const char *, bool))m->fn)(w, s->data, basValIsTruthy(args[1]));
+ basStringUnref(s);
+ }
+ return zeroValue();
}
}
}
@@ -1131,6 +1169,12 @@ void basFormRtDestroy(BasFormRtT *rt) {
arrfree(form->menuIdMap);
+ for (int32_t j = 0; j < form->popupMenuCount; j++) {
+ wmFreeMenu(form->popupMenus[j].menu);
+ }
+
+ arrfree(form->popupMenus);
+
if (form->window) {
dvxDestroyWindow(rt->ctx, form->window);
form->window = NULL;
@@ -1168,6 +1212,12 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
+ // Remember the last name asked for so a subsequent method/prop
+ // call that lands on a NULL ctrl can still name the culprit.
+ if (rt && ctrlName) {
+ snprintf(rt->lastLookupName, sizeof(rt->lastLookupName), "%s", ctrlName);
+ }
+
if (!form) {
return NULL;
}
@@ -1184,6 +1234,8 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
}
}
+ // (local miss is expected for cross-form access; don't spam the log)
+
// Search menu items on the current form
for (int32_t i = 0; i < form->menuIdMapCount; i++) {
if (strcasecmp(form->menuIdMap[i].name, ctrlName) == 0) {
@@ -1243,9 +1295,14 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index) {
- (void)ctx;
+ BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
+ if (rt && ctrlName) {
+ snprintf(rt->lastLookupName, sizeof(rt->lastLookupName),
+ "%s(%d)", ctrlName, (int)index);
+ }
+
if (!form) {
return NULL;
}
@@ -1385,7 +1442,8 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
if (!ctrl) {
basFormRtRuntimeError(rt,
"Read of unknown control",
- "Property: %s\nControl was not found on any loaded form.",
+ "Control: %s\nProperty: %s\nControl was not found on any loaded form.",
+ (rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?",
propName ? propName : "?");
return zeroValue();
}
@@ -1667,12 +1725,11 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
}
// Check the .frm cache for reload after unload.
- // Use a static guard to prevent recursion: basFormRtLoadFrm calls
- // basFormRtLoadForm for the "Begin Form" line, which would re-enter
- // this function. The guard lets the recursive call fall through to
- // bare form creation, which basFormRtLoadFrm then populates.
- static bool sLoadingFrm = false;
-
+ // sLoadingFrm (module-scope) prevents recursion: basFormRtLoadFrm
+ // calls basFormRtLoadForm via frmLoad_onFormBegin, which would
+ // re-enter this function. The guard lets the recursive call fall
+ // through to bare form creation, which basFormRtLoadFrm then
+ // populates.
if (!sLoadingFrm) {
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
@@ -1786,7 +1843,19 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
cbs.onCtrlEnd = frmLoad_onCtrlEnd;
cbs.onCtrlProp = frmLoad_onCtrlProp;
- if (!frmParse(source, sourceLen, &cbs)) {
+ // Set the guard so nested basFormRtLoadForm (fired from
+ // frmLoad_onFormBegin) skips the form's init code -- running it
+ // here would fire against an empty control list. The outer
+ // caller (IDE runModule or basFormRtLoadAllForms) runs module-
+ // level / init code after parsing finishes. Save+restore so
+ // nested basFormRtLoadForm -> basFormRtLoadFrm reentry still
+ // terminates the inner guard correctly.
+ bool savedLoadingFrm = sLoadingFrm;
+ sLoadingFrm = true;
+ bool parsed = frmParse(source, sourceLen, &cbs);
+ sLoadingFrm = savedLoadingFrm;
+
+ if (!parsed) {
arrfree(ctx.menuItems);
return NULL;
}
@@ -1801,59 +1870,93 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout);
}
- // Build menu bar from accumulated menu items
+ // Build menu bar + popup menus from accumulated menu items.
+ // A top-level Menu with Visible=False becomes a standalone
+ // popup (stored by name on the form). All other top-level
+ // menus go on the window's menu bar. Nested items are added
+ // to whichever top-level (bar or popup) is currently active.
int32_t menuCount = (int32_t)arrlen(menuItems);
if (menuCount > 0 && form->window) {
- MenuBarT *bar = wmAddMenuBar(form->window);
+ MenuBarT *bar = NULL;
+ bool haveVisibleTop = false;
- if (bar) {
- #define MENU_ID_BASE 10000
- MenuT *menuStack[16];
- memset(menuStack, 0, sizeof(menuStack));
-
- for (int32_t i = 0; i < menuCount; i++) {
- BasFrmMenuItemT *mi = &menuItems[i];
- bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
- bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);
-
- if (mi->level == 0) {
- // Top-level menu header
- menuStack[0] = wmAddMenu(bar, mi->caption);
- } else if (isSep && mi->level > 0 && menuStack[mi->level - 1]) {
- // Separator
- wmAddMenuSeparator(menuStack[mi->level - 1]);
- } else if (isSubParent && mi->level > 0 && menuStack[mi->level - 1]) {
- // Submenu parent
- menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
- } else if (mi->level > 0 && menuStack[mi->level - 1]) {
- // Regular menu item
- int32_t id = MENU_ID_BASE + i;
-
- if (mi->radioCheck) {
- wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
- } else if (mi->checked) {
- wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
- } else {
- wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
- }
-
- if (!mi->enabled) {
- wmMenuItemSetEnabled(bar, id, false);
- }
-
- // Store ID-to-name mapping for event dispatch
- BasMenuIdMapT map;
- memset(&map, 0, sizeof(map));
- map.id = id;
- snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
- arrput(form->menuIdMap, map);
- form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
- }
+ for (int32_t i = 0; i < menuCount; i++) {
+ if (menuItems[i].level == 0 && menuItems[i].visible) {
+ haveVisibleTop = true;
+ break;
}
-
- form->window->onMenu = onFormMenu;
}
+
+ if (haveVisibleTop) {
+ bar = wmAddMenuBar(form->window);
+ }
+
+ #define MENU_ID_BASE 10000
+ MenuT *menuStack[16];
+ memset(menuStack, 0, sizeof(menuStack));
+ bool topIsPopup = false;
+ MenuT *curTopPopup = NULL;
+
+ for (int32_t i = 0; i < menuCount; i++) {
+ BasFrmMenuItemT *mi = &menuItems[i];
+ bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
+ bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);
+
+ if (mi->level == 0) {
+ if (mi->visible) {
+ // Top-level menu header on the bar
+ menuStack[0] = bar ? wmAddMenu(bar, mi->caption) : NULL;
+ topIsPopup = false;
+ curTopPopup = NULL;
+ } else {
+ // Popup menu: allocate standalone, store by name.
+ curTopPopup = wmCreateMenu();
+ menuStack[0] = curTopPopup;
+ topIsPopup = true;
+
+ if (curTopPopup) {
+ BasFrmPopupMenuT entry;
+ memset(&entry, 0, sizeof(entry));
+ snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
+ entry.menu = curTopPopup;
+ arrput(form->popupMenus, entry);
+ form->popupMenuCount = (int32_t)arrlen(form->popupMenus);
+ }
+ }
+ } else if (isSep && menuStack[mi->level - 1]) {
+ wmAddMenuSeparator(menuStack[mi->level - 1]);
+ } else if (isSubParent && menuStack[mi->level - 1]) {
+ menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
+ } else if (menuStack[mi->level - 1]) {
+ int32_t id = MENU_ID_BASE + i;
+
+ if (mi->radioCheck) {
+ wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
+ } else if (mi->checked) {
+ wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
+ } else {
+ wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
+ }
+
+ if (!mi->enabled && bar && !topIsPopup) {
+ wmMenuItemSetEnabled(bar, id, false);
+ }
+
+ // Store ID-to-name mapping for event dispatch.
+ // Popup menu items use the same table -- the
+ // window's onMenu handler looks up by ID
+ // regardless of which menu the click came from.
+ BasMenuIdMapT map;
+ memset(&map, 0, sizeof(map));
+ map.id = id;
+ snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
+ arrput(form->menuIdMap, map);
+ form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
+ }
+ }
+
+ form->window->onMenu = onFormMenu;
}
arrfree(menuItems);
@@ -2021,6 +2124,16 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const cha
// control type, missing control reference, unknown method, etc.
// Silent no-ops here would hide these failures from the developer.
void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...) {
+ // If we're already in a terminated state, swallow follow-on errors
+ // silently. The modal message box pumps events, which can re-enter
+ // the form runtime and trip the same missing-control / missing-
+ // method path for every queued event. Showing the dialog again
+ // for each one stacks modals and spams the log; the first report
+ // is enough to point the user at the root cause.
+ if (rt && rt->terminated) {
+ return;
+ }
+
char details[512];
va_list ap;
@@ -2028,54 +2141,94 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
vsnprintf(details, sizeof(details), detailFmt, ap);
va_end(ap);
- // Log with a blank line and indent so it stands out among normal
- // log traffic.
- dvxLog("");
- dvxLog("BASIC RUNTIME ERROR: %s", summary ? summary : "(no summary)");
+ // Log the error only when we're also going to show the modal
+ // dialog. Hosts that suppress the dialog (the IDE) do their own
+ // reporting via the Output pane and editor navigation, and don't
+ // need a redundant log copy of every runtime error.
+ if (!rt || !rt->suppressErrorDialog) {
+ dvxLog("");
+ dvxLog("BASIC RUNTIME ERROR: %s", summary ? summary : "(no summary)");
- // Split details on newlines and indent each one.
- const char *p = details;
+ // Split details on newlines and indent each one.
+ const char *p = details;
- while (*p) {
- const char *nl = strchr(p, '\n');
- int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p);
- char line[256];
+ while (*p) {
+ const char *nl = strchr(p, '\n');
+ int32_t len = nl ? (int32_t)(nl - p) : (int32_t)strlen(p);
+ char line[256];
- if (len >= (int32_t)sizeof(line)) {
- len = sizeof(line) - 1;
+ if (len >= (int32_t)sizeof(line)) {
+ len = sizeof(line) - 1;
+ }
+
+ memcpy(line, p, len);
+ line[len] = '\0';
+ dvxLog(" %s", line);
+
+ if (!nl) {
+ break;
+ }
+
+ p = nl + 1;
}
-
- memcpy(line, p, len);
- line[len] = '\0';
- dvxLog(" %s", line);
-
- if (!nl) {
- break;
- }
-
- p = nl + 1;
}
- if (rt && rt->ctx) {
+ // Halt the VM and flip rt->terminated BEFORE showing the dialog.
+ // dvxMessageBox pumps events while it's open, and a single missing
+ // control typically has several queued events targeting it (load +
+ // activate + paint + ...). If we haven't marked the runtime as
+ // terminated yet, each re-entry into basFormRtRuntimeError passes
+ // the early-return check and stacks another dialog. Marking first
+ // means the re-entry sees terminated=true, returns early, and the
+ // user only has to dismiss one dialog. Setting vm->errorMsg makes
+ // basVmRun return BAS_VM_ERROR (not HALTED) so the IDE treats this
+ // as a runtime error, shows the diagnostic, and navigates the
+ // editor to vm->currentLine. ended=true also breaks the IDE's
+ // VB-style event pump loop.
+ if (rt && rt->vm) {
+ BasVmT *vm = rt->vm;
+
+ // Format: "Summary\nDetails". No embedded "on line N:" so the
+ // host (IDE) can prefix its own proc/line header without
+ // having to parse the line number back out of the middle.
+ // For the compiled stub, the host shows this as-is, which is
+ // fine -- the details string is the interesting part.
+ snprintf(vm->errorMsg, sizeof(vm->errorMsg),
+ "%s\n%s",
+ summary ? summary : "Runtime error", details);
+
+ // Snapshot PC and source line at the error site so the
+ // debugger can locate the owning SUB and line. runSubLoop
+ // restores vm->pc to the caller's saved PC on the way out (to
+ // keep RESUME semantics clean for ON ERROR), and
+ // vm->currentLine keeps being stomped by OP_LINE as other
+ // queued event handlers run. Both would otherwise be wrong
+ // by the time the IDE reads them after the loop exits.
+ //
+ // Prefer vm->currentOpPc over vm->pc: basVmStep advances
+ // vm->pc past the opcode AND its operands before dispatching
+ // the handler, so by the time we get here it can point at
+ // the first byte of the NEXT proc, which makes the proc
+ // lookup land on the wrong handler. currentOpPc is the PC
+ // of the opcode that triggered the host callback.
+ vm->errorPc = vm->currentOpPc > 0 ? vm->currentOpPc : vm->pc;
+ vm->errorLine = vm->currentLine;
+
+ vm->running = false;
+ vm->ended = true;
+ }
+
+ if (rt) {
+ rt->terminated = true;
+ }
+
+ if (rt && rt->ctx && !rt->suppressErrorDialog) {
char boxMsg[640];
snprintf(boxMsg, sizeof(boxMsg), "%s\n\n%s",
summary ? summary : "Runtime error",
details);
dvxMessageBox(rt->ctx, "BASIC Runtime Error", boxMsg, MB_OK | MB_ICONERROR);
}
-
- // Halt the VM so execution stops here rather than stumbling forward
- // with a corrupted state. Without this, a single missing control
- // can trigger a cascade of follow-on errors as subsequent property
- // accesses and method calls fail too. The `terminated` flag tells
- // basFormRtEventLoop to exit at its next pump -- otherwise the main
- // window would stay open with a dead VM behind it.
- if (rt) {
- if (rt->vm) {
- rt->vm->running = false;
- }
- rt->terminated = true;
- }
}
@@ -2157,10 +2310,16 @@ void basFormRtRunSimple(BasFormRtT *rt) {
} else if (result == BAS_VM_YIELDED) {
// DoEvents returned, continue
} else if (result == BAS_VM_ERROR) {
- const char *errMsg = basVmGetError(vm);
- char buf[512];
- snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error");
- dvxMessageBox(rt->ctx, "Error", buf, 0);
+ // basFormRtRuntimeError already surfaces its own error
+ // dialog and sets rt->terminated, so only show this generic
+ // box for VM-internal errors (division by zero, type
+ // mismatch, etc.) that didn't come through that path.
+ if (!rt->terminated) {
+ const char *errMsg = basVmGetError(vm);
+ char buf[512];
+ snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error");
+ dvxMessageBox(rt->ctx, "Error", buf, 0);
+ }
break;
} else {
break;
@@ -2206,7 +2365,8 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
if (!ctrl) {
basFormRtRuntimeError(rt,
"Assignment to unknown control",
- "Property: %s\nControl was not found on any loaded form.",
+ "Control: %s\nProperty: %s\nControl was not found on any loaded form.",
+ (rt && rt->lastLookupName[0]) ? rt->lastLookupName : "?",
propName ? propName : "?");
return;
}
@@ -2313,9 +2473,29 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT
return;
}
+ if (strcasecmp(propName, "ContextMenu") == 0) {
+ BasStringT *s = basValFormatString(value);
+ MenuT *m = NULL;
+
+ if (s->len > 0) {
+ BasFrmPopupMenuT *pm = findPopupMenu(frm, s->data);
+
+ if (pm) {
+ m = pm->menu;
+ }
+ }
+
+ if (win) {
+ win->contextMenu = m;
+ }
+
+ basStringUnref(s);
+ return;
+ }
+
basFormRtRuntimeError(rt,
"Unknown form property",
- "Form: %s\nProperty: %s\nValid: Caption, Visible, Width, Height, Left, Top, Resizable, AutoSize, Centered.",
+ "Form: %s\nProperty: %s\nValid: Caption, Visible, Width, Height, Left, Top, Resizable, AutoSize, Centered, ContextMenu.",
frm->name, propName ? propName : "?");
return;
}
@@ -2443,6 +2623,14 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
arrfree(form->controls);
form->controls = NULL;
+ for (int32_t i = 0; i < form->popupMenuCount; i++) {
+ wmFreeMenu(form->popupMenus[i].menu);
+ }
+
+ arrfree(form->popupMenus);
+ form->popupMenus = NULL;
+ form->popupMenuCount = 0;
+
if (form->window) {
if (rt->ctx->modalWindow == form->window) {
rt->ctx->modalWindow = NULL;
@@ -2555,6 +2743,40 @@ int32_t basPromptSave(const char *title) {
}
+// findPopupMenu -- look up a named popup menu on a form. Returns
+// NULL if no such menu exists.
+static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name) {
+ if (!form || !name) {
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < form->popupMenuCount; i++) {
+ if (strcasecmp(form->popupMenus[i].name, name) == 0) {
+ return &form->popupMenus[i];
+ }
+ }
+
+ return NULL;
+}
+
+
+// Allocate the next menu-item ID for a form. IDs are unique across
+// menu bar + popups on the same form and reused by menuIdMap for
+// event dispatch. Starts from MENU_ID_BASE + the highest in-use ID
+// so runtime-added items don't collide with .frm-loaded items.
+static int32_t nextMenuItemId(BasFormT *form) {
+ int32_t maxId = 10000 - 1; // MENU_ID_BASE - 1
+
+ for (int32_t i = 0; i < form->menuIdMapCount; i++) {
+ if (form->menuIdMap[i].id > maxId) {
+ maxId = form->menuIdMap[i].id;
+ }
+ }
+
+ return maxId + 1;
+}
+
+
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) {
if (strcasecmp(methodName, "SetFocus") == 0) {
wgtSetFocused(ctrl->widget);
@@ -2584,6 +2806,341 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas
return zeroValue();
}
+ // ========================================================
+ // Popup / context menu methods
+ // ========================================================
+ //
+ // Any control (and the synthetic form control) can show, build,
+ // or tear down popup menus owned by its form. Menus are named
+ // by string and stored on the form; IDs are shared with the
+ // menu-bar items so nameClick handlers fire regardless of which
+ // surface the user picked from.
+
+ BasFormT *menuForm = ctrl ? ctrl->form : NULL;
+
+ if (strcasecmp(methodName, "PopupMenu") == 0) {
+ // PopupMenu name$ [, x%, y%]
+ if (!menuForm || !menuForm->window || argc < 1) {
+ return zeroValue();
+ }
+
+ BasStringT *s = basValFormatString(args[0]);
+ BasFrmPopupMenuT *pm = findPopupMenu(menuForm, s->data);
+ basStringUnref(s);
+
+ if (!pm || !pm->menu) {
+ return zeroValue();
+ }
+
+ int32_t screenX = sFormRt ? sFormRt->ctx->mouseX : 0;
+ int32_t screenY = sFormRt ? sFormRt->ctx->mouseY : 0;
+
+ if (argc >= 3) {
+ // Explicit coords are relative to the control if we have
+ // one with a widget; form-level PopupMenu treats them as
+ // client-area coords translated through the window.
+ int32_t x = (int32_t)basValToNumber(args[1]);
+ int32_t y = (int32_t)basValToNumber(args[2]);
+
+ if (ctrl && ctrl->widget) {
+ screenX = ctrl->widget->x + x;
+ screenY = ctrl->widget->y + y;
+ } else if (menuForm->window) {
+ screenX = menuForm->window->x + menuForm->window->contentX + x;
+ screenY = menuForm->window->y + menuForm->window->contentY + y;
+ }
+ }
+
+ if (sFormRt && sFormRt->ctx) {
+ dvxShowContextMenu(sFormRt->ctx, menuForm->window, pm->menu, screenX, screenY);
+ }
+
+ return zeroValue();
+ }
+
+ if (strcasecmp(methodName, "CreateMenu") == 0) {
+ // CreateMenu name$ -- allocate an empty named popup menu.
+ // No-op if a menu with that name already exists.
+ if (!menuForm || argc < 1) {
+ return zeroValue();
+ }
+
+ BasStringT *s = basValFormatString(args[0]);
+
+ if (!findPopupMenu(menuForm, s->data)) {
+ BasFrmPopupMenuT entry;
+ memset(&entry, 0, sizeof(entry));
+ snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", s->data);
+ entry.menu = wmCreateMenu();
+ arrput(menuForm->popupMenus, entry);
+ menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus);
+ }
+
+ basStringUnref(s);
+ return zeroValue();
+ }
+
+ if (strcasecmp(methodName, "AddMenuItem") == 0) {
+ // AddMenuItem parentMenu$, itemName$, caption$
+ if (!menuForm || argc < 3) {
+ return zeroValue();
+ }
+
+ BasStringT *parent = basValFormatString(args[0]);
+ BasStringT *itemN = basValFormatString(args[1]);
+ BasStringT *capN = basValFormatString(args[2]);
+
+ BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data);
+
+ if (pm && pm->menu) {
+ int32_t id = nextMenuItemId(menuForm);
+ wmAddMenuItem(pm->menu, capN->data, id);
+
+ BasMenuIdMapT map;
+ memset(&map, 0, sizeof(map));
+ map.id = id;
+ snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", itemN->data);
+ arrput(menuForm->menuIdMap, map);
+ menuForm->menuIdMapCount = (int32_t)arrlen(menuForm->menuIdMap);
+
+ if (menuForm->window) {
+ menuForm->window->onMenu = onFormMenu;
+ }
+ }
+
+ basStringUnref(parent);
+ basStringUnref(itemN);
+ basStringUnref(capN);
+ return zeroValue();
+ }
+
+ if (strcasecmp(methodName, "AddMenuSeparator") == 0) {
+ // AddMenuSeparator parentMenu$
+ if (!menuForm || argc < 1) {
+ return zeroValue();
+ }
+
+ BasStringT *parent = basValFormatString(args[0]);
+ BasFrmPopupMenuT *pm = findPopupMenu(menuForm, parent->data);
+
+ if (pm && pm->menu) {
+ wmAddMenuSeparator(pm->menu);
+ }
+
+ basStringUnref(parent);
+ return zeroValue();
+ }
+
+ if (strcasecmp(methodName, "AddSubMenu") == 0) {
+ // AddSubMenu parentMenu$, childName$, caption$
+ // The new submenu can have AddMenuItem etc. called on it
+ // via `childName`.
+ if (!menuForm || argc < 3) {
+ return zeroValue();
+ }
+
+ BasStringT *parent = basValFormatString(args[0]);
+ BasStringT *childN = basValFormatString(args[1]);
+ BasStringT *capN = basValFormatString(args[2]);
+
+ BasFrmPopupMenuT *parentPm = findPopupMenu(menuForm, parent->data);
+
+ if (parentPm && parentPm->menu && !findPopupMenu(menuForm, childN->data)) {
+ MenuT *sub = wmAddSubMenu(parentPm->menu, capN->data);
+
+ if (sub) {
+ BasFrmPopupMenuT entry;
+ memset(&entry, 0, sizeof(entry));
+ snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", childN->data);
+ // NB: sub is owned by parentPm->menu (wmFreeMenu
+ // recurses); we store a non-owning reference here so
+ // AddMenuItem etc. can look it up by name. Don't
+ // wmFreeMenu the child on DestroyMenu for this
+ // name; only the root popup owns the tree.
+ entry.menu = sub;
+ arrput(menuForm->popupMenus, entry);
+ menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus);
+ }
+ }
+
+ basStringUnref(parent);
+ basStringUnref(childN);
+ basStringUnref(capN);
+ return zeroValue();
+ }
+
+ // ========================================================
+ // Toolbar runtime methods
+ // ========================================================
+ //
+ // Toolbars hold buttons, separators, and other children laid
+ // out horizontally. These convenience methods create the new
+ // widget, register it on the form by name (so BASIC code can
+ // do tbNew.Enabled = False afterwards), and tuck it into the
+ // toolbar. Equivalent manual construction uses
+ // CreateControl(form, "ImageButton", "tbNew", toolbar) -- these
+ // just shorten the common case.
+
+ bool isToolbar = (ctrl && strcasecmp(ctrl->typeName, "Toolbar") == 0);
+
+ if (isToolbar && strcasecmp(methodName, "AddButton") == 0) {
+ // AddButton name$, iconPath$
+ //
+ // Routed entirely through the generic runtime-creation +
+ // property-set path so formrt doesn't link against the
+ // ImageButton widget directly. CreateControl("ImageButton")
+ // resolves the widget via wgtFindByBasName at call time, and
+ // setProp "Picture" dispatches to whichever setter the
+ // ImageButton DXE registered in its iface.
+ if (!menuForm || !ctrl->widget || argc < 2) {
+ return zeroValue();
+ }
+
+ BasStringT *nameS = basValFormatString(args[0]);
+ BasStringT *pathS = basValFormatString(args[1]);
+
+ BasControlT *btn = (BasControlT *)basFormRtCreateCtrlEx(sFormRt, menuForm, "ImageButton", nameS->data, ctrl);
+
+ if (btn) {
+ BasValueT v = basValString(pathS);
+ basFormRtSetProp(sFormRt, btn, "Picture", v);
+ basValRelease(&v);
+ }
+
+ basStringUnref(nameS);
+ basStringUnref(pathS);
+ return zeroValue();
+ }
+
+ if (isToolbar && strcasecmp(methodName, "AddTextButton") == 0) {
+ // AddTextButton name$, caption$
+ if (!menuForm || !ctrl->widget || argc < 2) {
+ return zeroValue();
+ }
+
+ BasStringT *nameS = basValFormatString(args[0]);
+ BasStringT *capS = basValFormatString(args[1]);
+
+ BasControlT *btn = (BasControlT *)basFormRtCreateCtrlEx(sFormRt, menuForm, "CommandButton", nameS->data, ctrl);
+
+ if (btn) {
+ BasValueT v = basValString(capS);
+ basFormRtSetProp(sFormRt, btn, "Caption", v);
+ basValRelease(&v);
+ }
+
+ basStringUnref(nameS);
+ basStringUnref(capS);
+ return zeroValue();
+ }
+
+ if (isToolbar && strcasecmp(methodName, "AddSeparator") == 0) {
+ // "Line" resolves to the separator widget via the iface
+ // registry. The separator widget auto-orients to its parent
+ // (vertical inside a horizontal container, horizontal inside
+ // a vertical one), so we don't need a distinct VLine basName.
+ if (menuForm && ctrl->widget) {
+ basFormRtCreateCtrlEx(sFormRt, menuForm, "Line", "", ctrl);
+ wgtInvalidate(ctrl->widget);
+ }
+
+ return zeroValue();
+ }
+
+ if (isToolbar && strcasecmp(methodName, "Clear") == 0) {
+ // Destroy every child. Also remove their BasControlT
+ // entries from the form so stale name lookups don't linger.
+ if (!menuForm || !ctrl->widget) {
+ return zeroValue();
+ }
+
+ WidgetT *child = ctrl->widget->firstChild;
+
+ while (child) {
+ WidgetT *next = child->nextSibling;
+
+ // Remove matching BasControlT from the form's control
+ // list so its name becomes unknown again.
+ for (int32_t i = 0; i < (int32_t)arrlen(menuForm->controls); i++) {
+ if (menuForm->controls[i]->widget == child) {
+ free(menuForm->controls[i]->tooltip);
+ free(menuForm->controls[i]);
+ arrdel(menuForm->controls, i);
+ break;
+ }
+ }
+
+ wgtDestroy(child);
+ child = next;
+ }
+
+ wgtInvalidate(ctrl->widget);
+ return zeroValue();
+ }
+
+ if (isToolbar && strcasecmp(methodName, "ButtonCount") == 0) {
+ int32_t count = 0;
+
+ if (ctrl->widget) {
+ for (WidgetT *c = ctrl->widget->firstChild; c; c = c->nextSibling) {
+ count++;
+ }
+ }
+
+ return basValLong(count);
+ }
+
+ if (strcasecmp(methodName, "DestroyMenu") == 0) {
+ // DestroyMenu name$ -- free a top-level popup menu and
+ // remove its entry. Submenus are freed recursively as
+ // part of their owning parent; calling DestroyMenu on a
+ // submenu name only removes the lookup entry.
+ if (!menuForm || argc < 1) {
+ return zeroValue();
+ }
+
+ BasStringT *s = basValFormatString(args[0]);
+ int32_t foundIdx = -1;
+
+ for (int32_t i = 0; i < menuForm->popupMenuCount; i++) {
+ if (strcasecmp(menuForm->popupMenus[i].name, s->data) == 0) {
+ foundIdx = i;
+ break;
+ }
+ }
+
+ if (foundIdx >= 0) {
+ MenuT *m = menuForm->popupMenus[foundIdx].menu;
+
+ // Only free if this is a root (not a submenu whose
+ // parent also has a pointer). Heuristic: check if any
+ // other popup entry contains m as a descendant. Safe
+ // alternative: if m appears elsewhere in the list, treat
+ // the first occurrence as the root. For now we free
+ // only if no other popup has the same MenuT*, which
+ // handles the common case where a submenu is registered
+ // under a separate name but still owned by its parent.
+ bool isSubmenu = false;
+
+ for (int32_t i = 0; i < menuForm->popupMenuCount; i++) {
+ if (i != foundIdx && menuForm->popupMenus[i].menu == m) {
+ isSubmenu = true;
+ break;
+ }
+ }
+
+ if (!isSubmenu) {
+ wmFreeMenu(m);
+ }
+
+ arrdel(menuForm->popupMenus, foundIdx);
+ menuForm->popupMenuCount = (int32_t)arrlen(menuForm->popupMenus);
+ }
+
+ basStringUnref(s);
+ return zeroValue();
+ }
+
// Unknown method: raise a loud runtime error (visible MessageBox +
// DVX.LOG entry). Silent no-ops here used to hide typos in method
// names. This is a runtime safety net -- the parser should reject
@@ -3108,6 +3665,12 @@ static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventNa
static void frmLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) {
BasFrmLoadCtxT *ctx = (BasFrmLoadCtxT *)userData;
+ dvxLog("onCtrlBegin: type='%s' name='%s' form=%s nestDepth=%d",
+ typeName ? typeName : "?",
+ name ? name : "?",
+ (ctx->form && ctx->form->name[0]) ? ctx->form->name : "(null)",
+ (int)ctx->nestDepth);
+
if (!ctx->form || ctx->nestDepth <= 0) {
return;
}
@@ -3316,6 +3879,7 @@ static void frmLoad_onMenuBegin(void *userData, const char *name, int32_t level)
snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", name);
mi.level = level;
mi.enabled = true;
+ mi.visible = true;
arrput(ctx->menuItems, mi);
ctx->curMenuItemIdx = (int32_t)arrlen(ctx->menuItems) - 1;
ctx->current = NULL;
@@ -3352,6 +3916,10 @@ static void frmLoad_onMenuProp(void *userData, const char *key, const char *valu
} else if (strcasecmp(key, "Enabled") == 0) {
// Default-true: anything not "False" enables the item.
mip->enabled = (strcasecmp(text, "False") != 0);
+ } else if (strcasecmp(key, "Visible") == 0) {
+ // Top-level only: Visible=False marks the menu as a popup
+ // (not part of the menu bar). Ignored on nested items.
+ mip->visible = (strcasecmp(text, "False") != 0);
}
}
@@ -4618,6 +5186,31 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
return true;
}
+ if (strcasecmp(propName, "ContextMenu") == 0) {
+ // Set the auto-right-click menu. Empty string clears it.
+ BasStringT *s = basValFormatString(value);
+ MenuT *m = NULL;
+
+ if (s->len > 0 && ctrl->form) {
+ BasFrmPopupMenuT *pm = findPopupMenu(ctrl->form, s->data);
+
+ if (pm) {
+ m = pm->menu;
+ }
+ }
+
+ if (ctrl->widget) {
+ ctrl->widget->contextMenu = m;
+ } else if (ctrl->form && ctrl->form->window && ctrl == &ctrl->form->formCtrl) {
+ // Form-level ContextMenu: attach to the window so clicks
+ // outside any child widget still pop up the menu.
+ ctrl->form->window->contextMenu = m;
+ }
+
+ basStringUnref(s);
+ return true;
+ }
+
return false;
}
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
index 4d73fd6..227dbd0 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
@@ -62,6 +62,12 @@ typedef struct {
BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored)
} BasMenuIdMapT;
+// Named popup / context menu owned by a form.
+typedef struct {
+ char name[BAS_MAX_CTRL_NAME];
+ MenuT *menu; // standalone wmCreateMenu allocation, owned
+} BasFrmPopupMenuT;
+
// ============================================================
// Control instance (a widget on a form)
// ============================================================
@@ -129,6 +135,13 @@ typedef struct BasFormT {
// Menu ID to name mapping (for event dispatch)
BasMenuIdMapT *menuIdMap;
int32_t menuIdMapCount;
+ // Named popup (context) menus. Allocated standalone via
+ // wmCreateMenu() either from .frm `Begin Menu` with Visible=False
+ // or at runtime via CreateMenu/AddMenuItem BASIC calls. Click
+ // dispatch reuses menuIdMap above so Foo_Click-style handlers
+ // work the same as for menu-bar items. Freed on form unload.
+ BasFrmPopupMenuT *popupMenus; // stb_ds array
+ int32_t popupMenuCount;
// Synthetic control entry for the form itself, so that
// FormName.Property works through the same getProp/setProp path.
BasControlT formCtrl;
@@ -167,6 +180,21 @@ typedef struct {
// loop exits at the next pump so the app doesn't stumble forward
// with a halted VM.
bool terminated;
+ // Set true by hosts that want to handle runtime-error reporting
+ // themselves (the IDE shows the error in the output pane and
+ // navigates the editor to the line). When true,
+ // basFormRtRuntimeError still logs and halts but skips the modal
+ // MessageBox -- this removes a stray second OK press the IDE users
+ // were seeing when the dialog's event pump raced with other
+ // queued events.
+ bool suppressErrorDialog;
+ // Name of the most recent basFormRtFindCtrl lookup. The VM's
+ // OP_FIND_CTRL opcode discards the name string after the lookup,
+ // so when a subsequent OP_CALL_METHOD / OP_SET_PROP sees a NULL
+ // ctrl we'd otherwise have no way to tell the user WHICH control
+ // was missing. This is the last requested name; if the lookup
+ // succeeded this value is still the name of that control.
+ char lastLookupName[BAS_MAX_CTRL_NAME];
} BasFormRtT;
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
index 13787d1..14d1b77 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
@@ -616,6 +616,7 @@ static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level
snprintf(mi.name, DSGN_MAX_NAME, "%s", name);
mi.level = level;
mi.enabled = true;
+ mi.visible = true;
arrput(ctx->form->menuItems, mi);
ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1;
ctx->current = NULL;
@@ -653,6 +654,8 @@ static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *val
mip->radioCheck = frmParseBool(val);
} else if (strcasecmp(key, "Enabled") == 0) {
mip->enabled = frmParseBool(val);
+ } else if (strcasecmp(key, "Visible") == 0) {
+ mip->visible = (strcasecmp(val, "False") != 0);
}
}
@@ -1038,6 +1041,17 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n");
}
+ // Visible is only meaningful on top-level menus -- False
+ // marks it as a popup (not on the menu bar). Never
+ // serialize the default True.
+ if (mi->level == 0 && !mi->visible) {
+ for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
+ buf[pos++] = ' ';
+ }
+
+ pos += snprintf(buf + pos, bufSize - pos, "Visible = False\n");
+ }
+
curLevel = mi->level + 1;
}
diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
index 6bff7f3..1563af6 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
+++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
@@ -70,6 +70,7 @@ typedef struct {
bool checked;
bool radioCheck; // true = radio bullet instead of checkmark
bool enabled; // default true
+ bool visible; // default true; false on a top-level item = popup-only (not on menu bar)
} DsgnMenuItemT;
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
index ec544d7..108588f 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
@@ -603,6 +603,11 @@ static int32_t sOutputLen = 0;
// swaps directly between buffers with no splicing needed.
static char *sGeneralBuf = NULL; // (General) section: module-level code
static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
+// Cached copy of the source last passed to parseProcs (a .bas file's
+// contents, or a .frm's extracted code section). updateDropdowns
+// scans this so sProcTable.lineNum is in the same line-number space
+// prjMapLine returns -- getFullSource() packs blank lines and diverges.
+static char *sParsedSource = NULL;
static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit
@@ -625,7 +630,8 @@ static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
typedef struct {
char objName[64];
char evtName[64];
- int32_t lineNum;
+ int32_t lineNum; // line of the SUB / FUNCTION declaration
+ int32_t endLineNum; // line of the END SUB / END FUNCTION
} IdeProcEntryT;
static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array
@@ -1529,11 +1535,22 @@ static const char *ideValidator_lookupCtrlType(void *ctx, const char *ctrlName)
// Methods that exist on every widget via callCommonMethod() in formrt.c.
static bool ideValidator_isCommonMethod(const char *methodName) {
- return strcasecmp(methodName, "SetFocus") == 0 ||
- strcasecmp(methodName, "Refresh") == 0 ||
- strcasecmp(methodName, "SetReadOnly") == 0 ||
- strcasecmp(methodName, "SetEnabled") == 0 ||
- strcasecmp(methodName, "SetVisible") == 0;
+ return strcasecmp(methodName, "SetFocus") == 0 ||
+ strcasecmp(methodName, "Refresh") == 0 ||
+ strcasecmp(methodName, "SetReadOnly") == 0 ||
+ strcasecmp(methodName, "SetEnabled") == 0 ||
+ strcasecmp(methodName, "SetVisible") == 0 ||
+ strcasecmp(methodName, "PopupMenu") == 0 ||
+ strcasecmp(methodName, "CreateMenu") == 0 ||
+ strcasecmp(methodName, "AddMenuItem") == 0 ||
+ strcasecmp(methodName, "AddMenuSeparator") == 0 ||
+ strcasecmp(methodName, "AddSubMenu") == 0 ||
+ strcasecmp(methodName, "DestroyMenu") == 0 ||
+ strcasecmp(methodName, "AddButton") == 0 ||
+ strcasecmp(methodName, "AddTextButton") == 0 ||
+ strcasecmp(methodName, "AddSeparator") == 0 ||
+ strcasecmp(methodName, "Clear") == 0 ||
+ strcasecmp(methodName, "ButtonCount") == 0;
}
@@ -1560,6 +1577,7 @@ static bool ideValidator_isCommonProp(const char *propName) {
strcasecmp(propName, "Text") == 0 ||
strcasecmp(propName, "HelpTopic") == 0 ||
strcasecmp(propName, "ToolTipText") == 0 ||
+ strcasecmp(propName, "ContextMenu") == 0 ||
strcasecmp(propName, "DataSource") == 0 ||
strcasecmp(propName, "DataField") == 0 ||
strcasecmp(propName, "ListCount") == 0;
@@ -1620,25 +1638,28 @@ static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char
// Form-level properties
if (strcasecmp(wgtType, "Form") == 0) {
- return strcasecmp(propName, "Name") == 0 ||
- strcasecmp(propName, "Caption") == 0 ||
- strcasecmp(propName, "Width") == 0 ||
- strcasecmp(propName, "Height") == 0 ||
- strcasecmp(propName, "Left") == 0 ||
- strcasecmp(propName, "Top") == 0 ||
- strcasecmp(propName, "Visible") == 0 ||
- strcasecmp(propName, "Resizable") == 0 ||
- strcasecmp(propName, "AutoSize") == 0 ||
- strcasecmp(propName, "Centered") == 0 ||
- strcasecmp(propName, "Layout") == 0;
+ return strcasecmp(propName, "Name") == 0 ||
+ strcasecmp(propName, "Caption") == 0 ||
+ strcasecmp(propName, "Width") == 0 ||
+ strcasecmp(propName, "Height") == 0 ||
+ strcasecmp(propName, "Left") == 0 ||
+ strcasecmp(propName, "Top") == 0 ||
+ strcasecmp(propName, "Visible") == 0 ||
+ strcasecmp(propName, "Resizable") == 0 ||
+ strcasecmp(propName, "AutoSize") == 0 ||
+ strcasecmp(propName, "Centered") == 0 ||
+ strcasecmp(propName, "ContextMenu") == 0 ||
+ strcasecmp(propName, "Layout") == 0;
}
// Menu items
if (strcasecmp(wgtType, "Menu") == 0) {
- return strcasecmp(propName, "Name") == 0 ||
- strcasecmp(propName, "Checked") == 0 ||
- strcasecmp(propName, "Enabled") == 0 ||
- strcasecmp(propName, "Caption") == 0;
+ return strcasecmp(propName, "Name") == 0 ||
+ strcasecmp(propName, "Checked") == 0 ||
+ strcasecmp(propName, "Enabled") == 0 ||
+ strcasecmp(propName, "Visible") == 0 || // top-level: False = popup
+ strcasecmp(propName, "RadioCheck") == 0 ||
+ strcasecmp(propName, "Caption") == 0;
}
if (ideValidator_isCommonProp(propName)) {
@@ -2135,30 +2156,36 @@ static void debugNavigateToLine(int32_t concatLine) {
int32_t fileIdx = -1;
int32_t localLine = concatLine;
- // Map concatenated line to file and file-local line (code section)
+ // Map concatenated line to file and file-local line (code section).
+ // prjMapLine already returns a line relative to the code section
+ // (startLine in the source map is recorded AFTER the injected
+ // BEGINFORM directive), so no further adjustment is needed. The
+ // compile-error path uses the result as-is and this path should too.
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--;
- }
}
// Find which procedure we're in using the VM's PC and the compiled
// module's proc table, then build a dot-separated name for navigateToCodeLine.
+ // When a runtime error has halted the program, vm->pc has already
+ // been restored by runSubLoop to the caller's saved PC (outside the
+ // event handler that actually faulted), so use the snapshot
+ // captured in vm->errorPc instead. Without this, the proc lookup
+ // lands on whatever sub owns the saved PC -- typically "(General)"
+ // or the wrong handler -- and the editor jumps to the wrong place.
const char *procName = NULL;
char procBuf[128];
if (sVm && sDbgModule) {
const char *compiledName = NULL;
int32_t bestAddr = -1;
+ int32_t lookupPc = (sVm->errorMsg[0] && sVm->errorPc > 0)
+ ? sVm->errorPc : sVm->pc;
for (int32_t i = 0; i < sDbgModule->procCount; i++) {
int32_t addr = sDbgModule->procs[i].codeAddr;
- if (addr <= sVm->pc && addr > bestAddr) {
+ if (addr <= lookupPc && addr > bestAddr) {
bestAddr = addr;
compiledName = sDbgModule->procs[i].name;
}
@@ -3313,6 +3340,9 @@ static void freeProcBufs(void) {
free(sGeneralBuf);
sGeneralBuf = NULL;
+ free(sParsedSource);
+ sParsedSource = NULL;
+
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
free(sProcBufs[i]);
}
@@ -3747,6 +3777,8 @@ static void handleRunCmd(int32_t cmd) {
case CMD_OUTPUT_TO_LOG:
if (sWin && sWin->menuBar) {
sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG);
+ prefsSetBool(sPrefs, "run", "outputToLog", sOutputToLog);
+ prefsSave(sPrefs);
}
break;
@@ -3914,10 +3946,9 @@ static void handleWindowCmd(int32_t cmd) {
case CMD_WIN_TOOLBOX:
if (!sToolboxWin) {
- sToolboxWin = tbxCreate(sAc, &sDesigner);
+ sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom());
if (sToolboxWin) {
- sToolboxWin->y = toolbarBottom();
sToolboxWin->onMenu = onMenu;
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
}
@@ -4759,7 +4790,11 @@ static void loadFormCodeIntoEditor(void) {
// form runtime for execution.
static void loadFrmFiles(BasFormRtT *rt) {
+ dvxLog("loadFrmFiles: fileCount=%d", (int)sProject.fileCount);
+
for (int32_t i = 0; i < sProject.fileCount; i++) {
+ dvxLog(" file[%d] path=%s isForm=%d", (int)i, sProject.files[i].path, (int)sProject.files[i].isForm);
+
if (!sProject.files[i].isForm) {
continue;
}
@@ -4770,6 +4805,8 @@ static void loadFrmFiles(BasFormRtT *rt) {
int32_t bytesRead = 0;
char *frmBuf = platformReadFile(fullPath, &bytesRead);
+ dvxLog(" loadFrmFiles: fullPath=%s bytes=%d frmBuf=%s", fullPath, (int)bytesRead, frmBuf ? "ok" : "null");
+
if (!frmBuf) {
continue;
}
@@ -4782,6 +4819,9 @@ static void loadFrmFiles(BasFormRtT *rt) {
BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
free(frmBuf);
+ dvxLog(" loadFrmFiles: basFormRtLoadFrm returned form='%s'",
+ form && form->name[0] ? form->name : "(null)");
+
// Cache the form object name in the project file entry
if (form && form->name[0]) {
snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name);
@@ -7035,6 +7075,9 @@ static void openProject(void) {
static void parseProcs(const char *source) {
freeProcBufs();
+ free(sParsedSource);
+ sParsedSource = source ? strdup(source) : NULL;
+
if (!source) {
sGeneralBuf = strdup("");
return;
@@ -7597,6 +7640,16 @@ static void runModule(BasModuleT *mod) {
// Create form runtime (bridges UI opcodes to DVX widgets)
BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);
+ // The IDE surfaces runtime errors via the Output pane and jumps
+ // the editor to the faulting line, so suppress the form runtime's
+ // modal error dialog. Keeping the dialog on top of an already-
+ // modal-heavy IDE session was producing phantom "press OK twice"
+ // UX as the dialog's nested event pump raced with other queued
+ // events coming out of the user's program.
+ if (formRt) {
+ formRt->suppressErrorDialog = true;
+ }
+
sVm = vm;
sDbgFormRt = formRt;
sDbgModule = mod;
@@ -7673,18 +7726,18 @@ static void runModule(BasModuleT *mod) {
break;
}
- // Runtime error — navigate to error line
- int32_t pos = sOutputLen;
- int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm));
- sOutputLen += n;
- setOutputText(sOutputBuf);
+ // Module-level runtime error. For VM-internal errors (e.g.
+ // division by zero) there's no vm->errorLine set, so fall
+ // back to vm->currentLine -- but copy it into errorLine so
+ // the shared post-loop handler below can format and navigate
+ // uniformly with the event-handler error path.
+ if (vm->errorMsg[0] == '\0') {
+ const char *msg = basVmGetError(vm);
+ snprintf(vm->errorMsg, sizeof(vm->errorMsg), "%s", msg ? msg : "Runtime error");
+ }
- if (vm->currentLine > 0 && sEditor) {
- wgtTextAreaGoToLine(sEditor, vm->currentLine);
-
- if (sCodeWin && !sCodeWin->visible) {
- dvxShowWindow(sAc, sCodeWin);
- }
+ if (vm->errorLine == 0 && vm->currentLine > 0) {
+ vm->errorLine = vm->currentLine;
}
break;
@@ -7720,6 +7773,84 @@ static void runModule(BasModuleT *mod) {
}
}
+ // A runtime error fired from a host callback (e.g. setting a
+ // property on an unknown control) halts execution by writing
+ // vm->errorMsg and setting vm->ended. basVmRun surfaces that as
+ // BAS_VM_ERROR for module-level code, but errors raised inside an
+ // event handler just break the loop above. Either way, report
+ // the message and navigate the editor to the offending line so
+ // the user can fix it. debugNavigateToLine depends on sVm and
+ // sDbgModule, so it has to run BEFORE the cleanup block below.
+ // Use vm->errorLine (snapshot at error time) rather than
+ // vm->currentLine: the dialog/event pump can fire another handler
+ // whose OP_LINE overwrites currentLine before we read it here.
+ bool hadRuntimeError = (vm->errorMsg[0] != '\0') && sWin;
+ int32_t errorLine = hadRuntimeError ? vm->errorLine : 0;
+
+ if (hadRuntimeError) {
+ // Map the concatenated line to a file / proc-local line the
+ // same way compile errors do, so the Output message matches
+ // what the editor is actually showing.
+ int32_t errFileIdx = -1;
+ int32_t errLocalLine = errorLine;
+ const char *errFile = "";
+
+ if (errorLine > 0 && sProject.fileCount > 0 && sProject.sourceMapCount > 0) {
+ prjMapLine(&sProject, errorLine, &errFileIdx, &errLocalLine);
+
+ if (errFileIdx >= 0 && errFileIdx < sProject.fileCount) {
+ errFile = sProject.files[errFileIdx].path;
+ }
+ }
+
+ // Activate the file (populates sProcTable for that file), then
+ // find the proc whose body actually contains errLocalLine.
+ // A simple "last proc with lineNum <= errLocalLine" match is
+ // wrong when the error is module-level code between procs:
+ // it lands on whichever proc just finished instead of
+ // correctly reporting that the error is outside any sub.
+ // Use the proc's endLineNum to gate membership.
+ char procName[128] = {0};
+ int32_t procLine = errLocalLine;
+
+ if (errFileIdx >= 0) {
+ activateFile(errFileIdx, ViewCodeE);
+
+ int32_t procCount = (int32_t)arrlen(sProcTable);
+
+ for (int32_t i = 0; i < procCount; i++) {
+ if (errLocalLine >= sProcTable[i].lineNum &&
+ errLocalLine <= sProcTable[i].endLineNum) {
+ snprintf(procName, sizeof(procName), "%s.%s",
+ sProcTable[i].objName, sProcTable[i].evtName);
+ procLine = errLocalLine - sProcTable[i].lineNum + 1;
+ break;
+ }
+ }
+
+ navigateToCodeLine(errFileIdx, errLocalLine, procName[0] ? procName : NULL, false);
+ }
+
+ int32_t pos = sOutputLen;
+ int32_t n;
+
+ if (procName[0]) {
+ n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos,
+ "\nRUNTIME ERROR:\n%s line %d: %s\n",
+ procName, (int)procLine, vm->errorMsg);
+ } else if (errFile[0]) {
+ n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos,
+ "\nRUNTIME ERROR:\n%s line %d: %s\n",
+ errFile, (int)errLocalLine, vm->errorMsg);
+ } else {
+ n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos,
+ "\nRUNTIME ERROR:\nline %d: %s\n",
+ (int)errLocalLine, vm->errorMsg);
+ }
+
+ sOutputLen += n;
+ }
+
sVm = NULL;
sDbgFormRt = NULL;
sDbgModule = NULL;
@@ -7739,14 +7870,28 @@ static void runModule(BasModuleT *mod) {
updateProjectMenuState();
setOutputText(sOutputBuf);
- setStatus("Done.");
+ setStatus(hadRuntimeError ? "Runtime error." : "Done.");
- // Restore IDE windows
- if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); }
- if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); }
- if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); }
- if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); }
- if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); }
+ // Restore IDE windows. On a runtime error we want the code
+ // window on top so the user lands on the offending line; don't
+ // pop the form designer / toolbox / project back up, since that
+ // would cover the editor (dvxShowWindow raises). Output window
+ // stays up too so the error text is visible.
+ if (hadRuntimeError) {
+ if (sCodeWin && !sCodeWin->visible) {
+ dvxShowWindow(sAc, sCodeWin);
+ }
+
+ if (sCodeWin) {
+ dvxRaiseWindow(sAc, sCodeWin);
+ }
+ } else {
+ if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); }
+ if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); }
+ if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); }
+ if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); }
+ if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); }
+ }
// Repaint to clear destroyed runtime forms and restore designer
dvxUpdate(sAc);
@@ -8863,10 +9008,9 @@ static void switchToDesign(void) {
// Create toolbox and properties windows
if (!sToolboxWin) {
- sToolboxWin = tbxCreate(sAc, &sDesigner);
+ sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom());
if (sToolboxWin) {
- sToolboxWin->y = toolbarBottom();
sToolboxWin->onMenu = onMenu;
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
}
@@ -9273,8 +9417,16 @@ static void updateDropdowns(void) {
return;
}
- // Scan the reassembled full source
- const char *src = getFullSource();
+ // Scan the ORIGINAL parsed source (a .bas file or a .frm's code
+ // section) so line numbers match what prjMapLine returns.
+ // getFullSource() packs blank lines between procs, so its line
+ // numbering diverges from the file and breaks runtime error
+ // navigation and the navigateToCodeLine line translation.
+ const char *src = sParsedSource;
+
+ if (!src) {
+ src = getFullSource();
+ }
if (!src) {
return;
@@ -9306,15 +9458,19 @@ static void updateDropdowns(void) {
procName[nameLen] = '\0';
- // Find End Sub / End Function
+ // Find End Sub / End Function and record its line.
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
int32_t endTagLen = isSub ? 7 : 12;
const char *scan = pos;
+ int32_t scanLine = lineNum; // line of the Sub/Function declaration
+ int32_t endLineNum = 0;
while (*scan) {
const char *sl = dvxSkipWs(scan);
if (strncasecmp(sl, endTag, endTagLen) == 0) {
+ endLineNum = scanLine;
+
// Advance past the End line
while (*scan && *scan != '\n') {
scan++;
@@ -9322,6 +9478,7 @@ static void updateDropdowns(void) {
if (*scan == '\n') {
scan++;
+ scanLine++;
}
break;
@@ -9333,12 +9490,14 @@ static void updateDropdowns(void) {
if (*scan == '\n') {
scan++;
+ scanLine++;
}
}
IdeProcEntryT entry;
memset(&entry, 0, sizeof(entry));
- entry.lineNum = lineNum;
+ entry.lineNum = lineNum;
+ entry.endLineNum = endLineNum > 0 ? endLineNum : scanLine;
// Match proc name against known objects: form name,
// controls, menu items. Try each as a prefix followed
@@ -9813,6 +9972,9 @@ int32_t appMain(DxeAppContextT *ctx) {
if (sWin && sWin->menuBar) {
bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true);
wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
+
+ sOutputToLog = prefsGetBool(sPrefs, "run", "outputToLog", false);
+ wmMenuItemSetChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG, sOutputToLog);
}
// Load recent files list and populate menu
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
index 0a25898..9272e89 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
@@ -70,6 +70,7 @@ typedef struct {
WidgetT *checkedCb;
WidgetT *radioCheckCb;
WidgetT *enabledCb;
+ WidgetT *popupCb; // Top-level popup (hidden from menu bar)
WidgetT *listBox;
} MnuEdStateT;
@@ -169,6 +170,13 @@ static void applyFields(void) {
mi->checked = wgtCheckboxIsChecked(sMed.checkedCb);
mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb);
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
+ // Popup checkbox only meaningful on top-level items; for nested
+ // items `visible` stays true (the field is ignored there).
+ if (mi->level == 0 && sMed.popupCb) {
+ mi->visible = !wgtCheckboxIsChecked(sMed.popupCb);
+ } else {
+ mi->visible = true;
+ }
}
@@ -198,6 +206,10 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, false);
wgtCheckboxSetChecked(sMed.radioCheckCb, false);
wgtCheckboxSetChecked(sMed.enabledCb, true);
+ if (sMed.popupCb) {
+ wgtCheckboxSetChecked(sMed.popupCb, false);
+ wgtSetEnabled(sMed.popupCb, false);
+ }
sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
return;
}
@@ -209,6 +221,11 @@ static void loadFields(void) {
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
+ if (sMed.popupCb) {
+ // Popup (Visible=False) only applies to top-level menus.
+ wgtCheckboxSetChecked(sMed.popupCb, mi->level == 0 && !mi->visible);
+ wgtSetEnabled(sMed.popupCb, mi->level == 0);
+ }
}
@@ -230,6 +247,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
mi.enabled = true;
+ mi.visible = true;
arrput(sMed.items, mi);
}
@@ -271,6 +289,7 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
+ sMed.popupCb = wgtCheckbox(chkRow, "Popup");
wgtCheckboxSetChecked(sMed.enabledCb, true);
// Listbox
@@ -444,6 +463,7 @@ static void onInsert(WidgetT *w) {
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
mi.enabled = true;
+ mi.visible = true;
// Insert after the current item's subtree, at the same level
int32_t insertAt;
@@ -583,6 +603,7 @@ static void onNext(WidgetT *w) {
DsgnMenuItemT mi;
memset(&mi, 0, sizeof(mi));
mi.enabled = true;
+ mi.visible = true;
if (count > 0) {
mi.level = sMed.items[count - 1].level;
diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
index 196a5d9..47cb770 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
@@ -95,11 +95,11 @@ static void onToolClick(WidgetT *w) {
}
-WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds) {
+WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
sDs = ds;
arrsetlen(sTbxTools, 0);
- WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, 30, TBX_WIN_W, TBX_WIN_H, false);
+ WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, y, TBX_WIN_W, TBX_WIN_H, false);
if (!win) {
return NULL;
diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.h b/src/apps/kpunch/dvxbasic/ide/ideToolbox.h
index e4366db..70bb2e6 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.h
+++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.h
@@ -27,8 +27,12 @@
#include "ideDesigner.h"
-// Create the toolbox floating window. Returns the WindowT pointer.
-WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds);
+// Create the toolbox floating window at the given y coordinate.
+// Caller owns y so the window is painted at its final location
+// during the asynchronous icon-loading dvxUpdate cycles; moving
+// the window after creation would leave stale paint at the old
+// position on the backbuffer. Returns the WindowT pointer.
+WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y);
// Destroy the toolbox window.
void tbxDestroy(AppContextT *ctx, WindowT *win);
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c
index 82bb006..ecd4513 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.c
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.c
@@ -327,6 +327,8 @@ void basVmReset(BasVmT *vm) {
vm->errorNumber = 0;
vm->errorPc = 0;
vm->errorNextPc = 0;
+ vm->errorLine = 0;
+ vm->currentOpPc = 0;
vm->inErrorHandler = false;
vm->errorMsg[0] = '\0';
}
@@ -391,6 +393,17 @@ BasVmResultE basVmRun(BasVmT *vm) {
}
}
+ // Host callbacks (property setters, method dispatch, etc.) can
+ // abort execution by writing an error message and clearing
+ // vm->running. The step itself returns OK because the VM did
+ // nothing wrong; the failure happened on the host side. If the
+ // host left an error message, surface it as BAS_VM_ERROR so the
+ // caller (IDE, stub) can navigate to the line and stop the
+ // program instead of treating the halt as a normal END.
+ if (vm->errorMsg[0] != '\0') {
+ return BAS_VM_ERROR;
+ }
+
return BAS_VM_HALTED;
}
@@ -467,7 +480,8 @@ BasVmResultE basVmStep(BasVmT *vm) {
return BAS_VM_HALTED;
}
- uint8_t op = vm->module->code[vm->pc++];
+ vm->currentOpPc = vm->pc;
+ uint8_t op = vm->module->code[vm->pc++];
switch (op) {
case OP_NOP:
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h
index 4fa9946..7d4888a 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.h
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.h
@@ -389,6 +389,7 @@ typedef struct {
int32_t stepLimit; // max steps per basVmRun (0 = unlimited)
int32_t stepCount; // steps executed in last basVmRun
int32_t currentLine; // source line from last OP_LINE (debugger)
+ int32_t currentOpPc; // PC of the instruction currently executing (set at start of each basVmStep; use this, not vm->pc, for "which SUB are we in" lookups from host callbacks, since vm->pc has already advanced past the opcode + operands)
// Debug state
int32_t *breakpoints; // sorted line numbers (host-managed)
@@ -428,6 +429,7 @@ typedef struct {
int32_t errorNumber; // current Err number
int32_t errorPc; // PC of the instruction that caused the error (for RESUME)
int32_t errorNextPc; // PC of the next instruction after error (for RESUME NEXT)
+ int32_t errorLine; // source line where the error was raised (frozen; debuggers use this because currentLine keeps advancing while the error dialog pumps events)
bool inErrorHandler; // true when executing error handler code
char errorMsg[BAS_VM_ERROR_MSG_LEN]; // current error description
diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.c b/src/apps/kpunch/dvxhelp/dvxhelp.c
index 5d4aea7..3988889 100644
--- a/src/apps/kpunch/dvxhelp/dvxhelp.c
+++ b/src/apps/kpunch/dvxhelp/dvxhelp.c
@@ -703,7 +703,11 @@ static void helpHeadingCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinH = top + textH + HELP_HEADING1_PAD * 2;
} else if (hd->level == 2) {
int32_t top = first ? 0 : HELP_HEADING2_TOP;
- w->calcMinH = top + textH + HELP_HEADING2_PAD + 2;
+ bool nextHasBorder = (w->nextSibling &&
+ (w->nextSibling->type == sHelpCodeTypeId ||
+ w->nextSibling->type == sHelpRuleTypeId));
+ int32_t extra = nextHasBorder ? 0 : 2;
+ w->calcMinH = top + textH + HELP_HEADING2_PAD + extra;
} else {
int32_t top = first ? 0 : HELP_HEADING3_TOP;
w->calcMinH = top + textH;
@@ -738,8 +742,19 @@ static void helpHeadingPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const
int32_t top = first ? 0 : HELP_HEADING2_TOP;
int32_t textY = w->y + top;
drawTextN(d, ops, font, w->x + HELP_CONTENT_PAD, textY, hd->text, textLen, colors->contentFg, 0, false);
- int32_t lineY = textY + font->charHeight + 1;
- drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow);
+
+ // Suppress the underline when the next sibling paints its own
+ // top border, so we don't end up with two nearly-adjacent lines.
+ // Tables and code blocks both use sHelpCodeTypeId; .hr uses
+ // sHelpRuleTypeId.
+ bool nextHasBorder = (w->nextSibling &&
+ (w->nextSibling->type == sHelpCodeTypeId ||
+ w->nextSibling->type == sHelpRuleTypeId));
+
+ if (!nextHasBorder) {
+ int32_t lineY = textY + font->charHeight + 1;
+ drawHLine(d, ops, w->x + HELP_CONTENT_PAD, lineY, w->w - HELP_CONTENT_PAD * 2, colors->windowShadow);
+ }
} else {
int32_t top = first ? 0 : HELP_HEADING3_TOP;
int32_t textY = w->y + top;
diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c
index 2457551..f475939 100644
--- a/src/apps/kpunch/progman/progman.c
+++ b/src/apps/kpunch/progman/progman.c
@@ -132,6 +132,7 @@ static int32_t sAppCount = 0;
int32_t appMain(DxeAppContextT *ctx);
void appShutdown(void);
+static int32_t appEntryCmpName(const void *a, const void *b);
static void buildPmWindow(void);
static void desktopUpdate(void);
static void onAppButtonClick(WidgetT *w);
@@ -166,6 +167,14 @@ void appShutdown(void) {
}
+// qsort comparator for AppEntryT -- case-insensitive on display name.
+static int32_t appEntryCmpName(const void *a, const void *b) {
+ const AppEntryT *ea = (const AppEntryT *)a;
+ const AppEntryT *eb = (const AppEntryT *)b;
+ return strcasecmp(ea->name, eb->name);
+}
+
+
// Build the main Program Manager window with app buttons, menus, and status bar.
// Window is centered horizontally and placed in the upper quarter vertically
// so spawned app windows don't hide behind it.
@@ -435,6 +444,11 @@ static void scanAppsDir(void) {
sAppFiles = NULL;
sAppCount = 0;
scanAppsDirRecurse("apps");
+
+ if (sAppCount > 1) {
+ qsort(sAppFiles, sAppCount, sizeof(AppEntryT), (int (*)(const void *, const void *))appEntryCmpName);
+ }
+
dvxLog("Progman: found %ld app(s)", (long)sAppCount);
}
diff --git a/src/apps/kpunch/resedit/resedit.frm b/src/apps/kpunch/resedit/resedit.frm
index f71226b..12f2cfe 100644
--- a/src/apps/kpunch/resedit/resedit.frm
+++ b/src/apps/kpunch/resedit/resedit.frm
@@ -106,12 +106,8 @@ ResEdit.Show
ResList.SetColumns "Name,20|Type,8|Size,12"
LblStatus.Caption = "Ready. Use File > Open to load a DXE file."
-
-' ============================================================
-' Type name helper
-' ============================================================
-
FUNCTION TypeName$(t AS LONG)
+ ' Type name helper
IF t = RES_TYPE_ICON THEN
TypeName$ = "Icon"
ELSEIF t = RES_TYPE_TEXT THEN
@@ -123,12 +119,8 @@ FUNCTION TypeName$(t AS LONG)
END IF
END FUNCTION
-
-' ============================================================
-' Format a byte size for display
-' ============================================================
-
FUNCTION FormatSize$(sz AS LONG)
+ ' Format a byte size for display
IF sz < 1024 THEN
FormatSize$ = STR$(sz) + " B"
ELSE
@@ -136,12 +128,8 @@ FUNCTION FormatSize$(sz AS LONG)
END IF
END FUNCTION
-
-' ============================================================
-' Refresh the resource list from the open handle
-' ============================================================
-
SUB RefreshList
+ ' Refresh the resource list from the open handle
ResList.Clear
IF resHandle = 0 THEN
@@ -161,12 +149,8 @@ SUB RefreshList
LblStatus.Caption = filePath + " - " + STR$(n) + " resource(s)"
END SUB
-
-' ============================================================
-' Close the current file
-' ============================================================
-
SUB CloseFile
+ ' Close the current file
IF resHandle <> 0 THEN
ResClose resHandle
resHandle = 0
@@ -183,12 +167,8 @@ SUB CloseFile
LblStatus.Caption = "No file loaded."
END SUB
-
-' ============================================================
-' Reopen the file (after modification) and refresh
-' ============================================================
-
SUB ReopenAndRefresh
+ ' Reopen the file (after modification) and refresh
DIM path AS STRING
path = filePath
@@ -210,12 +190,8 @@ SUB ReopenAndRefresh
END IF
END SUB
-
-' ============================================================
-' Enable/disable selection-dependent menus
-' ============================================================
-
SUB UpdateMenuState
+ ' Enable/disable selection-dependent menus
DIM hasSel AS INTEGER
hasSel = (ResList.ListIndex >= 0)
mnuExtract.Enabled = hasSel
@@ -223,11 +199,6 @@ SUB UpdateMenuState
mnuEditText.Enabled = hasSel
END SUB
-
-' ============================================================
-' Menu handlers
-' ============================================================
-
SUB mnuOpen_Click
DIM path AS STRING
path = basFileOpen("Open DXE File", "Applications (*.app)|Widget Modules (*.wgt)|Libraries (*.lib)|All Files (*.*)")
@@ -429,11 +400,6 @@ SUB mnuRemove_Click
END IF
END SUB
-
-' ============================================================
-' ListView selection change
-' ============================================================
-
SUB ResList_Click
UpdateMenuState
END SUB
diff --git a/src/apps/kpunch/widshow/widshow.frm b/src/apps/kpunch/widshow/widshow.frm
index 63d7fbd..d9446b5 100644
--- a/src/apps/kpunch/widshow/widshow.frm
+++ b/src/apps/kpunch/widshow/widshow.frm
@@ -200,8 +200,11 @@ Begin Form WidgetShow
End
Begin StatusBar sbar
- Caption = "Ready."
Weight = 0
+ Begin Label lblStatus
+ Caption = "Ready."
+ Weight = 1
+ End
End
End
@@ -233,12 +236,6 @@ OPTION EXPLICIT
DIM addCount AS INTEGER
-
-SUB setStatus(msg AS STRING)
- sbar.Caption = msg
-END SUB
-
-
Load WidgetShow
WidgetShow.Show
@@ -279,12 +276,13 @@ tvTree.AddChildItem 4, "Pine"
tvTree.SetExpanded 0, -1
tvTree.SetExpanded 4, -1
-setStatus "Ready. Hover widgets for descriptions."
+lblStatus.Caption = "Ready. Hover widgets for descriptions."
-' ============================================================
-' Event handlers
-' ============================================================
+SUB setStatus(msg AS STRING)
+ lblStatus.Caption = msg
+END SUB
+
SUB tabs_Click
setStatus "Tab: " + STR$(tabs.GetActive())
@@ -353,14 +351,14 @@ SUB btnDel_Click
END SUB
-SUB lstItems_Click
+SUB lstItems_Change
IF lstItems.ListIndex >= 0 THEN
setStatus "ListBox: " + lstItems.List(lstItems.ListIndex)
END IF
END SUB
-SUB lvFiles_Click
+SUB lvFiles_Change
DIM i AS INTEGER
FOR i = 0 TO lvFiles.RowCount() - 1
IF lvFiles.IsItemSelected(i) THEN
@@ -371,7 +369,7 @@ SUB lvFiles_Click
END SUB
-SUB tvTree_Click
+SUB tvTree_Change
DIM i AS INTEGER
FOR i = 0 TO tvTree.ItemCount() - 1
IF tvTree.IsItemSelected(i) THEN
diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
index f86b853..ab6bfb5 100644
--- a/src/libs/kpunch/dvxshell/shellMain.c
+++ b/src/libs/kpunch/dvxshell/shellMain.c
@@ -217,9 +217,6 @@ int shellMain(int argc, char *argv[]) {
// roughly twice as many scheduling turns as any single app.
tsSetPriority(0, TS_PRIORITY_HIGH);
- // Gather system information (CPU, memory, drives, etc.)
- shellInfoInit(&sCtx);
-
// Initialize app slot table
shellAppInit();
@@ -266,6 +263,10 @@ int shellMain(int argc, char *argv[]) {
platformVideoEnumModes(logVideoMode, NULL);
dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);
+ // Gather system information (CPU, memory, video, drives, etc.)
+ // after dvxInit so the display fields have real values to report.
+ shellInfoInit(&sCtx);
+
// Apply mouse preferences
const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", MOUSE_WHEEL_DIR_DEFAULT);
int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1;
diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
index 4b9525c..274b019 100644
--- a/src/libs/kpunch/libdvx/dvxApp.c
+++ b/src/libs/kpunch/libdvx/dvxApp.c
@@ -2105,6 +2105,11 @@ static void invalidateAllWindows(AppContextT *ctx) {
// between top-level menus. Position is clamped to screen edges so the
// popup doesn't go off-screen.
+void dvxShowContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
+ openContextMenu(ctx, win, menu, screenX, screenY);
+}
+
+
static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY) {
if (!menu || menu->itemCount <= 0) {
return;
@@ -3917,6 +3922,15 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH);
}
+ // Clear the modal pointer if it was this window. Callers that open
+ // modal dialogs are supposed to restore the previous modal before
+ // destroying the dialog window, but a crash / longjmp / missed
+ // cleanup can leave modalWindow pointing at freed memory -- every
+ // subsequent click is then dropped by the modal gate.
+ if (ctx->modalWindow == win) {
+ ctx->modalWindow = NULL;
+ }
+
wmDestroyWindow(&ctx->stack, win);
// Dirty icon area again with the updated count (one fewer icon)
@@ -4209,6 +4223,13 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
// Call the window's paint callback to update the content buffer
// before marking the screen dirty. This means raw-paint apps only
// need to call dvxInvalidateWindow -- onPaint fires automatically.
+ // Set PAINT_FULL for widget-managed windows: widgetOnPaint only
+ // relays out + background-clears when paintNeeded >= PAINT_FULL,
+ // and callers invalidating "the whole window" (theme changes,
+ // scroll, color-scheme swaps) need that full repaint rather than
+ // the dirty-widget-only partial repaint the default would give.
+ win->paintNeeded = PAINT_FULL;
+
if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
index 805511f..538270a 100644
--- a/src/libs/kpunch/libdvx/dvxApp.h
+++ b/src/libs/kpunch/libdvx/dvxApp.h
@@ -238,6 +238,14 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win);
// Raise a window to the top of the z-order and give it focus.
void dvxRaiseWindow(AppContextT *ctx, WindowT *win);
+// Display a context menu at a screen position. `menu` must be a
+// standalone MenuT allocated with wmCreateMenu() and populated with
+// wmAddMenuItem() / wmAddMenuSeparator() / wmAddSubMenu(). `win`
+// supplies the onMenu dispatch target; menu-item clicks fire
+// win->onMenu(win, id) exactly like menu-bar items. Position is
+// clamped to screen edges.
+void dvxShowContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t screenX, int32_t screenY);
+
// Resize a window to exactly fit its widget tree's computed minimum size
// (plus chrome). Used for dialog boxes and other fixed-layout windows
// where the window should shrink-wrap its content.
diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h
index 3b13ef7..6a0f724 100644
--- a/src/libs/kpunch/libdvx/dvxWgt.h
+++ b/src/libs/kpunch/libdvx/dvxWgt.h
@@ -634,6 +634,8 @@ const void *wgtGetApi(const char *name);
#define WGT_SIG_RET_STR 18 // const char *fn(const WidgetT *)
#define WGT_SIG_STR_INT 19 // void fn(WidgetT *, const char *, int32_t)
#define WGT_SIG_INT_STR 20 // void fn(WidgetT *, int32_t, const char *)
+#define WGT_SIG_STR_STR 21 // void fn(WidgetT *, const char *, const char *)
+#define WGT_SIG_STR_BOOL 22 // void fn(WidgetT *, const char *, bool)
// Property descriptor
typedef struct {
diff --git a/src/libs/kpunch/libdvx/widgetEvent.c b/src/libs/kpunch/libdvx/widgetEvent.c
index f85f6e8..8120c2a 100644
--- a/src/libs/kpunch/libdvx/widgetEvent.c
+++ b/src/libs/kpunch/libdvx/widgetEvent.c
@@ -636,15 +636,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) {
(void)orient;
(void)value;
- // Repaint with new scroll position. PAINT_FULL is required so
- // widgetOnPaint re-lays out children at the new root x/y offset;
- // otherwise children keep their old positions and the content
- // does not visibly scroll.
+ // Repaint with new scroll position. dvxInvalidateWindow sets
+ // PAINT_FULL so widgetOnPaint re-lays out children at the new
+ // root x/y offset.
if (win->widgetRoot) {
AppContextT *ctx = wgtGetContext(win->widgetRoot);
if (ctx) {
- win->paintNeeded = PAINT_FULL;
dvxInvalidateWindow(ctx, win);
}
}
diff --git a/src/libs/kpunch/texthelp/textHelp.c b/src/libs/kpunch/texthelp/textHelp.c
index 74ae63a..b97dfb5 100644
--- a/src/libs/kpunch/texthelp/textHelp.c
+++ b/src/libs/kpunch/texthelp/textHelp.c
@@ -40,6 +40,15 @@
#define CURSOR_BLINK_MS 250
static clock_t sCursorBlinkTime = 0;
+// Slack added to line cache capacity when growing. Bigger values
+// mean fewer reallocs at the cost of wasted space; 256 empirically
+// keeps realloc traffic low on medium-sized file edits.
+#define TEXT_EDIT_LINE_CAP_GROWTH 256
+
+// Default tab width when a cache has been left at 0 (callers should
+// always initialize with textEditLineCacheInit, but guard defensively).
+#define TEXT_EDIT_DEFAULT_TAB_WIDTH 3
+
// Track the widget that last had an active selection so we can
// clear it in O(1) instead of walking every widget in every window.
static WidgetT *sLastSelectedWidget = NULL;
@@ -52,11 +61,43 @@ void clearOtherSelections(WidgetT *except);
static bool clearSelectionOnWidget(WidgetT *w);
bool isWordChar(char c);
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
+void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
+void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol);
+bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
+int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize);
+void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor);
+void textEditLineCacheDirty(TextEditLineCacheT *lc);
+void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len);
+void textEditLineCacheFree(TextEditLineCacheT *lc);
+void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth);
+void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen);
+void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen);
+static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len);
+void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
+void textEditMultiFree(TextEditMultiT *te);
+bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
+int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len);
+int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
+int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
+int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len);
+void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col);
+int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
+int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col);
void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
+uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
+int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
+int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
static void textHelpInit(void) __attribute__((constructor));
void wgtUpdateCursorBlink(void);
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd);
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect);
+void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor);
+void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
+void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts);
+void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks);
+void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
+int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
+int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth);
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX);
int32_t wordBoundaryLeft(const char *buf, int32_t pos);
@@ -151,6 +192,622 @@ static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor,
}
+// Paint `text` as a run of single-color regions defined by
+// syntaxColors[textOff..textOff+len). Coalesces adjacent same-color
+// bytes to minimize drawTextN calls.
+void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors) {
+ int32_t runStart = 0;
+
+ while (runStart < len) {
+ uint8_t curColor = syntaxColors[textOff + runStart];
+ int32_t runEnd = runStart + 1;
+
+ while (runEnd < len && syntaxColors[textOff + runEnd] == curColor) {
+ runEnd++;
+ }
+
+ uint32_t fg = textEditSyntaxColor(d, curColor, defaultFg, customColors);
+ drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true);
+ runStart = runEnd;
+ }
+}
+
+
+void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol) {
+ int32_t curLineOff = textEditLineStart(lc, buf, len, cursorRow);
+ int32_t curOff = curLineOff + cursorCol;
+
+ if (curOff > len) {
+ curOff = len;
+ }
+
+ int32_t col = textEditVisualCol(buf, curLineOff, curOff, tabW);
+
+ if (cursorRow < *pScrollRow) {
+ *pScrollRow = cursorRow;
+ }
+
+ if (cursorRow >= *pScrollRow + visRows) {
+ *pScrollRow = cursorRow - visRows + 1;
+ }
+
+ if (col < *pScrollCol) {
+ *pScrollCol = col;
+ }
+
+ if (col >= *pScrollCol + visCols) {
+ *pScrollCol = col - visCols + 1;
+ }
+}
+
+
+// Searches forward (or backward) from the cursor for `needle`. On
+// match, selects it and moves the cursor to match start (forward)
+// or match end (backward). No wrap-around.
+bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor) {
+ if (!needle || !needle[0]) {
+ return false;
+ }
+
+ int32_t needleLen = strlen(needle);
+
+ if (needleLen > len) {
+ return false;
+ }
+
+ int32_t cursorByte = textEditRowColToOff(lc, buf, len, *pCursorRow, *pCursorCol);
+ int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
+ int32_t searchLen = len - needleLen + 1;
+
+ if (searchLen <= 0) {
+ return false;
+ }
+
+ int32_t count = forward ? (searchLen - startPos) : (startPos + 1);
+
+ if (count <= 0) {
+ return false;
+ }
+
+ for (int32_t attempt = 0; attempt < count; attempt++) {
+ int32_t pos = forward ? startPos + attempt : startPos - attempt;
+
+ if (pos < 0 || pos >= searchLen) {
+ break;
+ }
+
+ bool match;
+
+ if (caseSensitive) {
+ match = (memcmp(buf + pos, needle, needleLen) == 0);
+ } else {
+ match = (strncasecmp(buf + pos, needle, needleLen) == 0);
+ }
+
+ if (match) {
+ *pSelAnchor = pos;
+ *pSelCursor = pos + needleLen;
+
+ int32_t cursorOff = forward ? pos : pos + needleLen;
+ textEditOffToRowCol(lc, buf, len, cursorOff, pCursorRow, pCursorCol);
+ *pDesiredCol = *pCursorCol;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// Writes the word under the cursor (if any) into out and returns
+// its length. Empty string if the cursor is not on a word.
+int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize) {
+ out[0] = '\0';
+
+ if (!buf || len == 0 || outSize < 2) {
+ return 0;
+ }
+
+ int32_t off = textEditRowColToOff(lc, buf, len, cursorRow, cursorCol);
+ int32_t ws = wordStart(buf, off);
+ int32_t we = wordEnd(buf, len, off);
+ int32_t wdLen = we - ws;
+
+ if (wdLen <= 0) {
+ return 0;
+ }
+
+ if (wdLen >= outSize) {
+ wdLen = outSize - 1;
+ }
+
+ memcpy(out, buf + ws, wdLen);
+ out[wdLen] = '\0';
+ return wdLen;
+}
+
+
+// Jumps to the given 1-based line, selects the whole line, and
+// scrolls so the line sits ~1/4 from the top of the visible area.
+void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor) {
+ int32_t totalLines = textEditLineCount(lc, buf, len);
+ int32_t row = line - 1;
+
+ if (row < 0) {
+ row = 0;
+ }
+
+ if (row >= totalLines) {
+ row = totalLines - 1;
+ }
+
+ *pCursorRow = row;
+ *pCursorCol = 0;
+ *pDesiredCol = 0;
+
+ int32_t lineStart = textEditLineStart(lc, buf, len, row);
+ int32_t lineL = textEditLineLen(lc, buf, len, row);
+ *pSelAnchor = lineStart;
+ *pSelCursor = lineStart + lineL;
+
+ int32_t targetScroll = row - visRows / 4;
+
+ if (targetScroll < 0) {
+ targetScroll = 0;
+ }
+
+ int32_t maxScroll = totalLines - visRows;
+
+ if (maxScroll < 0) {
+ maxScroll = 0;
+ }
+
+ if (targetScroll > maxScroll) {
+ targetScroll = maxScroll;
+ }
+
+ *pScrollRow = targetScroll;
+ *pScrollCol = 0;
+}
+
+
+void textEditLineCacheDirty(TextEditLineCacheT *lc) {
+ lc->cachedLines = -1;
+ lc->cachedMaxLL = -1;
+}
+
+
+void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len) {
+ if (lc->cachedLines < 0) {
+ textEditLineCacheRebuild(lc, buf, len);
+ }
+}
+
+
+void textEditLineCacheFree(TextEditLineCacheT *lc) {
+ free(lc->lineOffsets);
+ free(lc->lineVisLens);
+ lc->lineOffsets = NULL;
+ lc->lineVisLens = NULL;
+ lc->lineOffsetCap = 0;
+ lc->lineVisLenCap = 0;
+ lc->cachedLines = -1;
+ lc->cachedMaxLL = -1;
+}
+
+
+void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth) {
+ lc->lineOffsets = NULL;
+ lc->lineVisLens = NULL;
+ lc->lineOffsetCap = 0;
+ lc->lineVisLenCap = 0;
+ lc->cachedLines = -1;
+ lc->cachedMaxLL = -1;
+ lc->tabWidth = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
+}
+
+
+// Incrementally update the cache after deleting bytes at `off`.
+// Caller passes the POST-delete buffer and length. Falls back to a
+// full rebuild if the deletion may have spanned a newline.
+void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen) {
+ if (lc->cachedLines < 0 || deleteLen <= 0) {
+ textEditLineCacheDirty(lc);
+ return;
+ }
+
+ // We only trust the incremental path for single-byte deletes; any
+ // longer run could have swallowed a newline, and the post-delete
+ // buffer can't tell us whether it did.
+ if (deleteLen > 1) {
+ textEditLineCacheDirty(lc);
+ return;
+ }
+
+ int32_t line = 0;
+
+ for (line = lc->cachedLines - 1; line > 0; line--) {
+ if (lc->lineOffsets[line] <= off) {
+ break;
+ }
+ }
+
+ for (int32_t i = line + 1; i <= lc->cachedLines; i++) {
+ lc->lineOffsets[i] -= deleteLen;
+ }
+
+ int32_t lineOff = lc->lineOffsets[line];
+ int32_t nextOff = (line + 1 <= lc->cachedLines) ? lc->lineOffsets[line + 1] : len;
+
+ if (line + 1 <= lc->cachedLines && nextOff <= lineOff) {
+ textEditLineCacheDirty(lc);
+ return;
+ }
+
+ for (int32_t i = lineOff; i < nextOff && i < len; i++) {
+ if (buf[i] == '\n') {
+ if (i < nextOff - 1) {
+ textEditLineCacheDirty(lc);
+ return;
+ }
+ }
+ }
+
+ int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
+ int32_t vc = 0;
+
+ for (int32_t i = lineOff; i < nextOff && i < len && buf[i] != '\n'; i++) {
+ if (buf[i] == '\t') {
+ vc += tabW - (vc % tabW);
+ } else {
+ vc++;
+ }
+ }
+
+ lc->lineVisLens[line] = vc;
+ lc->cachedMaxLL = -1;
+}
+
+
+// Incrementally update the cache after inserting bytes at `off`.
+// Caller passes the POST-insert buffer and length. Falls back to a
+// full rebuild if the insertion contains newlines.
+void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen) {
+ if (lc->cachedLines < 0 || insertLen <= 0) {
+ textEditLineCacheDirty(lc);
+ return;
+ }
+
+ for (int32_t i = off; i < off + insertLen && i < len; i++) {
+ if (buf[i] == '\n') {
+ textEditLineCacheDirty(lc);
+ return;
+ }
+ }
+
+ int32_t line = 0;
+
+ for (line = lc->cachedLines - 1; line > 0; line--) {
+ if (lc->lineOffsets[line] <= off) {
+ break;
+ }
+ }
+
+ for (int32_t i = line + 1; i <= lc->cachedLines; i++) {
+ lc->lineOffsets[i] += insertLen;
+ }
+
+ int32_t lineOff = lc->lineOffsets[line];
+ int32_t lineEnd = (line + 1 <= lc->cachedLines) ? lc->lineOffsets[line + 1] : len;
+ int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
+ int32_t vc = 0;
+
+ for (int32_t i = lineOff; i < lineEnd && buf[i] != '\n'; i++) {
+ if (buf[i] == '\t') {
+ vc += tabW - (vc % tabW);
+ } else {
+ vc++;
+ }
+ }
+
+ lc->lineVisLens[line] = vc;
+
+ if (vc > lc->cachedMaxLL) {
+ lc->cachedMaxLL = vc;
+ } else {
+ lc->cachedMaxLL = -1;
+ }
+}
+
+
+// Single O(N) scan populates line-offset table plus per-line
+// visual lengths. lineOffsets[lineCount] is the buffer-end sentinel.
+static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len) {
+ int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
+ int32_t lineCount = 1;
+
+ for (int32_t i = 0; i < len; i++) {
+ if (buf[i] == '\n') {
+ lineCount++;
+ }
+ }
+
+ int32_t needed = lineCount + 1;
+
+ if (needed > lc->lineOffsetCap) {
+ int32_t newCap = needed + TEXT_EDIT_LINE_CAP_GROWTH;
+ lc->lineOffsets = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t));
+ lc->lineOffsetCap = newCap;
+ }
+
+ if (lineCount > lc->lineVisLenCap) {
+ int32_t newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH;
+ lc->lineVisLens = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t));
+ lc->lineVisLenCap = newCap;
+ }
+
+ int32_t line = 0;
+ int32_t vc = 0;
+ int32_t maxVL = 0;
+
+ lc->lineOffsets[0] = 0;
+
+ for (int32_t i = 0; i < len; i++) {
+ if (buf[i] == '\n') {
+ lc->lineVisLens[line] = vc;
+
+ if (vc > maxVL) {
+ maxVL = vc;
+ }
+
+ line++;
+ lc->lineOffsets[line] = i + 1;
+ vc = 0;
+ } else if (buf[i] == '\t') {
+ vc += tabW - (vc % tabW);
+ } else {
+ vc++;
+ }
+ }
+
+ lc->lineVisLens[line] = vc;
+
+ if (vc > maxVL) {
+ maxVL = vc;
+ }
+
+ lc->lineOffsets[lineCount] = len;
+ lc->cachedLines = lineCount;
+ lc->cachedMaxLL = maxVL;
+}
+
+
+void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth) {
+ int32_t newW = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
+
+ if (lc->tabWidth == newW) {
+ return;
+ }
+
+ lc->tabWidth = newW;
+ textEditLineCacheDirty(lc);
+}
+
+
+void textEditMultiFree(TextEditMultiT *te) {
+ free(te->buf);
+ free(te->undoBuf);
+ textEditLineCacheFree(&te->lines);
+ te->buf = NULL;
+ te->undoBuf = NULL;
+ te->bufSize = 0;
+ te->len = 0;
+}
+
+
+// Allocates the buf/undoBuf pair sized for maxLen characters plus
+// NUL, and initializes cursor/scroll/selection/undo state. Returns
+// false (leaving te->buf == NULL) if allocation fails.
+bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) {
+ memset(te, 0, sizeof(*te));
+
+ int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
+
+ te->buf = (char *)malloc(bufSize);
+ te->undoBuf = (char *)malloc(bufSize);
+ te->bufSize = bufSize;
+
+ if (!te->buf || !te->undoBuf) {
+ free(te->buf);
+ free(te->undoBuf);
+ te->buf = NULL;
+ te->undoBuf = NULL;
+ return false;
+ }
+
+ te->buf[0] = '\0';
+ te->undoBuf[0] = '\0';
+ te->selAnchor = -1;
+ te->selCursor = -1;
+ textEditLineCacheInit(&te->lines, tabWidth);
+ return true;
+}
+
+
+int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len) {
+ textEditLineCacheEnsure(lc, buf, len);
+ return lc->cachedLines;
+}
+
+
+int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row) {
+ textEditLineCacheEnsure(lc, buf, len);
+
+ if (row < 0 || row >= lc->cachedLines) {
+ return 0;
+ }
+
+ int32_t start = lc->lineOffsets[row];
+ int32_t next = lc->lineOffsets[row + 1];
+
+ if (next > start && buf[next - 1] == '\n') {
+ next--;
+ }
+
+ return next - start;
+}
+
+
+int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row) {
+ textEditLineCacheEnsure(lc, buf, len);
+
+ if (row < 0) {
+ return 0;
+ }
+
+ if (row >= lc->cachedLines) {
+ return len;
+ }
+
+ return lc->lineOffsets[row];
+}
+
+
+int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len) {
+ textEditLineCacheEnsure(lc, buf, len);
+
+ if (lc->cachedMaxLL < 0 && lc->cachedLines >= 0) {
+ int32_t maxVL = 0;
+
+ for (int32_t i = 0; i < lc->cachedLines; i++) {
+ if (lc->lineVisLens[i] > maxVL) {
+ maxVL = lc->lineVisLens[i];
+ }
+ }
+
+ lc->cachedMaxLL = maxVL;
+ }
+
+ return lc->cachedMaxLL;
+}
+
+
+void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col) {
+ textEditLineCacheEnsure(lc, buf, len);
+
+ if (!lc->lineOffsets || lc->cachedLines < 0) {
+ int32_t r = 0;
+ int32_t c = 0;
+
+ for (int32_t i = 0; i < off; i++) {
+ if (buf[i] == '\n') {
+ r++;
+ c = 0;
+ } else {
+ c++;
+ }
+ }
+
+ *row = r;
+ *col = c;
+ return;
+ }
+
+ int32_t lo = 0;
+ int32_t hi = lc->cachedLines;
+
+ while (lo < hi) {
+ int32_t mid = (lo + hi + 1) / 2;
+
+ if (lc->lineOffsets[mid] <= off) {
+ lo = mid;
+ } else {
+ hi = mid - 1;
+ }
+ }
+
+ *row = lo;
+ *col = off - lc->lineOffsets[lo];
+}
+
+
+// Replaces every occurrence of needle with replacement. Records a
+// single undo snapshot before mutating. Clamps the cursor to the
+// post-replace buffer length and clears any selection. Returns the
+// count of replacements made.
+int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) {
+ if (!needle || !needle[0] || !replacement) {
+ return 0;
+ }
+
+ int32_t needleLen = strlen(needle);
+ int32_t replLen = strlen(replacement);
+ int32_t delta = replLen - needleLen;
+ int32_t count = 0;
+
+ if (needleLen > *pLen) {
+ return 0;
+ }
+
+ textEditSaveUndo(buf, *pLen, textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol), undoBuf, pUndoLen, pUndoCursor, bufSize);
+
+ int32_t pos = 0;
+
+ while (pos + needleLen <= *pLen) {
+ bool match;
+
+ if (caseSensitive) {
+ match = (memcmp(buf + pos, needle, needleLen) == 0);
+ } else {
+ match = (strncasecmp(buf + pos, needle, needleLen) == 0);
+ }
+
+ if (match) {
+ if (*pLen + delta >= bufSize) {
+ break;
+ }
+
+ if (delta != 0) {
+ memmove(buf + pos + replLen, buf + pos + needleLen, *pLen - pos - needleLen);
+ }
+
+ memcpy(buf + pos, replacement, replLen);
+ *pLen += delta;
+ buf[*pLen] = '\0';
+ count++;
+ pos += replLen;
+ } else {
+ pos++;
+ }
+ }
+
+ if (count > 0) {
+ *pSelAnchor = -1;
+ *pSelCursor = -1;
+
+ int32_t cursorOff = textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol);
+
+ if (cursorOff > *pLen) {
+ textEditOffToRowCol(lc, buf, *pLen, *pLen, pCursorRow, pCursorCol);
+ }
+
+ *pDesiredCol = *pCursorCol;
+ textEditLineCacheDirty(lc);
+ }
+
+ return count;
+}
+
+
+int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col) {
+ int32_t start = textEditLineStart(lc, buf, len, row);
+ int32_t lineL = textEditLineLen(lc, buf, len, row);
+ int32_t clamp = col < lineL ? col : lineL;
+
+ return start + clamp;
+}
+
+
void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize) {
if (!undoBuf) {
return;
@@ -164,6 +821,68 @@ void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int
}
+// Resolve a syntax color index to a packed-pixel foreground color.
+// Falls back to defaultFg for TEXT_SYNTAX_DEFAULT or unknown indices.
+// A non-zero entry in custom[idx] (0x00RRGGBB) overrides the default.
+uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) {
+ if (idx != TEXT_SYNTAX_DEFAULT && idx < TEXT_SYNTAX_MAX && custom && custom[idx]) {
+ uint32_t c = custom[idx];
+ return packColor(d, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
+ }
+
+ switch (idx) {
+ case TEXT_SYNTAX_KEYWORD: return packColor(d, 0, 0, 128);
+ case TEXT_SYNTAX_STRING: return packColor(d, 128, 0, 0);
+ case TEXT_SYNTAX_COMMENT: return packColor(d, 0, 128, 0);
+ case TEXT_SYNTAX_NUMBER: return packColor(d, 128, 0, 128);
+ case TEXT_SYNTAX_OPERATOR: return packColor(d, 128, 128, 0);
+ case TEXT_SYNTAX_TYPE: return packColor(d, 0, 128, 128);
+ default: return defaultFg;
+ }
+}
+
+
+// Tab-aware visual column from buffer offset within a line.
+// tabW <= 0 means tabs are 1 column (no expansion).
+int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW) {
+ int32_t vc = 0;
+
+ for (int32_t i = lineStart; i < off; i++) {
+ if (buf[i] == '\t' && tabW > 0) {
+ vc += tabW - (vc % tabW);
+ } else {
+ vc++;
+ }
+ }
+
+ return vc;
+}
+
+
+// Tab-aware: convert visual column to buffer offset within a line.
+int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW) {
+ int32_t vc = 0;
+ int32_t i = lineStart;
+
+ while (i < len && buf[i] != '\n') {
+ int32_t w = 1;
+
+ if (buf[i] == '\t' && tabW > 0) {
+ w = tabW - (vc % tabW);
+ }
+
+ if (vc + w > targetVC) {
+ break;
+ }
+
+ vc += w;
+ i++;
+ }
+
+ return i;
+}
+
+
static void textHelpInit(void) {
sCursorBlinkFn = wgtUpdateCursorBlink;
}
@@ -262,6 +981,901 @@ void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLe
}
+// Called during a drag to auto-scroll when the mouse moves past the
+// top or bottom of the visible area, and to extend the selection to
+// the new cursor position.
+void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor) {
+ int32_t relX = vx - textX;
+ int32_t relY = vy - textY;
+ int32_t totalLines = textEditLineCount(lc, buf, len);
+
+ if (relY < 0 && *pScrollRow > 0) {
+ (*pScrollRow)--;
+ } else if (relY >= visRows * font->charHeight && *pScrollRow < totalLines - visRows) {
+ (*pScrollRow)++;
+ }
+
+ int32_t clickRow = *pScrollRow + relY / font->charHeight;
+ int32_t clickVisCol = scrollCol + relX / font->charWidth;
+
+ if (clickRow < 0) {
+ clickRow = 0;
+ }
+
+ if (clickRow >= totalLines) {
+ clickRow = totalLines - 1;
+ }
+
+ if (clickVisCol < 0) {
+ clickVisCol = 0;
+ }
+
+ int32_t dragLineStart = textEditLineStart(lc, buf, len, clickRow);
+ int32_t dragByteOff = textEditVisualColToOff(buf, len, dragLineStart, clickVisCol, tabW);
+ int32_t clickCol = dragByteOff - dragLineStart;
+ int32_t lineL = textEditLineLen(lc, buf, len, clickRow);
+
+ if (clickCol > lineL) {
+ clickCol = lineL;
+ }
+
+ *pCursorRow = clickRow;
+ *pCursorCol = clickCol;
+ *pDesiredCol = clickCol;
+ *pSelCursor = textEditRowColToOff(lc, buf, len, clickRow, clickCol);
+}
+
+
+// Handle a mouse click on the text content area. Caller has already
+// excluded scrollbar and gutter clicks. Single click places cursor
+// and starts a drag anchor; double-click selects the word under
+// the cursor; triple-click selects the whole line.
+void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor) {
+ int32_t totalLines = textEditLineCount(lc, buf, len);
+ int32_t relX = vx - textX;
+ int32_t relY = vy - textY;
+ int32_t clickRow = scrollRow + relY / font->charHeight;
+ int32_t clickVisCol = scrollCol + relX / font->charWidth;
+
+ if (clickRow < 0) {
+ clickRow = 0;
+ }
+
+ if (clickRow >= totalLines) {
+ clickRow = totalLines - 1;
+ }
+
+ if (clickVisCol < 0) {
+ clickVisCol = 0;
+ }
+
+ int32_t clkLineStart = textEditLineStart(lc, buf, len, clickRow);
+ int32_t clkByteOff = textEditVisualColToOff(buf, len, clkLineStart, clickVisCol, tabW);
+ int32_t clickCol = clkByteOff - clkLineStart;
+ int32_t lineL = textEditLineLen(lc, buf, len, clickRow);
+
+ if (clickCol > lineL) {
+ clickCol = lineL;
+ }
+
+ int32_t clicks = multiClickDetect(vx, vy);
+
+ if (clicks >= 3) {
+ int32_t lineStart = textEditRowColToOff(lc, buf, len, clickRow, 0);
+ int32_t lineEnd = textEditRowColToOff(lc, buf, len, clickRow, lineL);
+
+ *pCursorRow = clickRow;
+ *pCursorCol = lineL;
+ *pDesiredCol = lineL;
+ *pSelAnchor = lineStart;
+ *pSelCursor = lineEnd;
+ sDragWidget = NULL;
+ return;
+ }
+
+ if (clicks == 2 && buf) {
+ int32_t off = textEditRowColToOff(lc, buf, len, clickRow, clickCol);
+ int32_t ws = wordStart(buf, off);
+ int32_t we = wordEnd(buf, len, off);
+ int32_t weRow;
+ int32_t weCol;
+ textEditOffToRowCol(lc, buf, len, we, &weRow, &weCol);
+
+ *pCursorRow = weRow;
+ *pCursorCol = weCol;
+ *pDesiredCol = weCol;
+ *pSelAnchor = ws;
+ *pSelCursor = we;
+ sDragWidget = NULL;
+ return;
+ }
+
+ *pCursorRow = clickRow;
+ *pCursorCol = clickCol;
+ *pDesiredCol = clickCol;
+
+ int32_t anchorOff = textEditRowColToOff(lc, buf, len, clickRow, clickCol);
+ *pSelAnchor = anchorOff;
+ *pSelCursor = anchorOff;
+ sDragWidget = w;
+}
+
+
+// Core multi-line text editing engine. Parameterized on state
+// pointers so that the widget's fields remain its own; the library
+// never dereferences TextAreaDataT or similar.
+void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts) {
+ int32_t *pRow = pCursorRow;
+ int32_t *pCol = pCursorCol;
+ int32_t *pSA = pSelAnchor;
+ int32_t *pSC = pSelCursor;
+ bool shift = (mod & KEY_MOD_SHIFT) != 0;
+ int32_t tabW = opts->tabWidth > 0 ? opts->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
+
+ #define CUR_OFF() textEditRowColToOff(lc, buf, *pLen, *pRow, *pCol)
+ #define ENSURE_VIS() textEditEnsureVisible(lc, buf, *pLen, *pRow, *pCol, tabW, visRows, visCols, pScrollRow, pScrollCol)
+ #define FROM_OFF(o) textEditOffToRowCol(lc, buf, *pLen, (o), pRow, pCol)
+ #define SEL_BEGIN() do { \
+ if (shift && *pSA < 0) { *pSA = CUR_OFF(); *pSC = *pSA; } \
+ } while (0)
+ #define SEL_END() do { \
+ if (shift) { *pSC = CUR_OFF(); } \
+ else { *pSA = -1; *pSC = -1; } \
+ } while (0)
+ #define HAS_SEL() (*pSA >= 0 && *pSC >= 0 && *pSA != *pSC)
+ #define SEL_LO() (*pSA < *pSC ? *pSA : *pSC)
+ #define SEL_HI() (*pSA < *pSC ? *pSC : *pSA)
+
+ if (HAS_SEL()) {
+ if (*pSA > *pLen) {
+ *pSA = *pLen;
+ }
+
+ if (*pSC > *pLen) {
+ *pSC = *pLen;
+ }
+ }
+
+ // Ctrl+A -- select all
+ if (key == 1) {
+ *pSA = 0;
+ *pSC = *pLen;
+ FROM_OFF(*pLen);
+ *pDesiredCol = *pCol;
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+C -- copy
+ if (key == KEY_CTRL_C) {
+ if (HAS_SEL()) {
+ clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
+ }
+
+ return;
+ }
+
+ if (opts->readOnly) {
+ goto navigation;
+ }
+
+ // Ctrl+V -- paste
+ if (key == KEY_CTRL_V) {
+ int32_t clipLen = 0;
+ const char *clip = clipboardGet(&clipLen);
+
+ if (clipLen > 0) {
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+
+ if (HAS_SEL()) {
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ }
+
+ int32_t off = CUR_OFF();
+ int32_t canFit = bufSize - 1 - *pLen;
+ int32_t paste = clipLen < canFit ? clipLen : canFit;
+
+ if (paste > 0) {
+ memmove(buf + off + paste, buf + off, *pLen - off + 1);
+ memcpy(buf + off, clip, paste);
+ *pLen += paste;
+ FROM_OFF(off + paste);
+ *pDesiredCol = *pCol;
+ }
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+
+ textEditLineCacheDirty(lc);
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+X -- cut
+ if (key == KEY_CTRL_X) {
+ if (HAS_SEL()) {
+ clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ *pDesiredCol = *pCol;
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+
+ textEditLineCacheDirty(lc);
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+Z -- undo (single-slot swap; gives you one level of redo)
+ if (key == KEY_CTRL_Z) {
+ if (undoBuf && *pUndoLen >= 0) {
+ char tmpBuf[*pLen + 1];
+ int32_t tmpLen = *pLen;
+ int32_t tmpCursor = CUR_OFF();
+ int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;
+
+ memcpy(tmpBuf, buf, copyLen);
+ tmpBuf[copyLen] = '\0';
+
+ int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1;
+ memcpy(buf, undoBuf, restLen);
+ buf[restLen] = '\0';
+ *pLen = restLen;
+
+ int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
+ memcpy(undoBuf, tmpBuf, saveLen);
+ undoBuf[saveLen] = '\0';
+ *pUndoLen = saveLen;
+ *pUndoCursor = tmpCursor;
+
+ int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
+ *pUndoCursor = tmpCursor;
+ FROM_OFF(restoreOff);
+ *pDesiredCol = *pCol;
+ *pSA = -1;
+ *pSC = -1;
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+
+ textEditLineCacheDirty(lc);
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Enter -- insert newline (optionally copies leading whitespace)
+ if (key == 0x0D) {
+ if (*pLen < bufSize - 1) {
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+
+ if (HAS_SEL()) {
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ }
+
+ int32_t off = CUR_OFF();
+ int32_t indent = 0;
+ char indentBuf[64];
+
+ if (opts->autoIndent) {
+ int32_t lineStart = textEditLineStart(lc, buf, *pLen, *pRow);
+
+ while (lineStart + indent < off && indent < 63 && (buf[lineStart + indent] == ' ' || buf[lineStart + indent] == '\t')) {
+ indentBuf[indent] = buf[lineStart + indent];
+ indent++;
+ }
+ }
+
+ if (*pLen + 1 + indent < bufSize) {
+ memmove(buf + off + 1 + indent, buf + off, *pLen - off + 1);
+ buf[off] = '\n';
+ memcpy(buf + off + 1, indentBuf, indent);
+ *pLen += 1 + indent;
+ (*pRow)++;
+ *pCol = indent;
+ *pDesiredCol = indent;
+ }
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+
+ textEditLineCacheDirty(lc);
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Backspace
+ if (key == 8) {
+ if (HAS_SEL()) {
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ *pDesiredCol = *pCol;
+ textEditLineCacheDirty(lc);
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+ } else {
+ int32_t off = CUR_OFF();
+
+ if (off > 0) {
+ char deleted = buf[off - 1];
+ textEditSaveUndo(buf, *pLen, off, undoBuf, pUndoLen, pUndoCursor, bufSize);
+ memmove(buf + off - 1, buf + off, *pLen - off + 1);
+ (*pLen)--;
+ FROM_OFF(off - 1);
+ *pDesiredCol = *pCol;
+
+ if (deleted == '\n') {
+ textEditLineCacheDirty(lc);
+ } else {
+ textEditLineCacheNotifyDelete(lc, buf, *pLen, off - 1, 1);
+ }
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+ }
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Delete
+ if (key == KEY_DELETE) {
+ if (HAS_SEL()) {
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ *pDesiredCol = *pCol;
+ textEditLineCacheDirty(lc);
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+ } else {
+ int32_t off = CUR_OFF();
+
+ if (off < *pLen) {
+ char deleted = buf[off];
+ textEditSaveUndo(buf, *pLen, off, undoBuf, pUndoLen, pUndoCursor, bufSize);
+ memmove(buf + off, buf + off + 1, *pLen - off);
+ (*pLen)--;
+
+ if (deleted == '\n') {
+ textEditLineCacheDirty(lc);
+ } else {
+ textEditLineCacheNotifyDelete(lc, buf, *pLen, off, 1);
+ }
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+ }
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+navigation: {
+ int32_t totalLines = textEditLineCount(lc, buf, *pLen);
+
+ // Left arrow
+ if (key == KEY_LEFT) {
+ SEL_BEGIN();
+ int32_t off = CUR_OFF();
+
+ if (off > 0) {
+ FROM_OFF(off - 1);
+ }
+
+ *pDesiredCol = *pCol;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Right arrow
+ if (key == KEY_RIGHT) {
+ SEL_BEGIN();
+ int32_t off = CUR_OFF();
+
+ if (off < *pLen) {
+ FROM_OFF(off + 1);
+ }
+
+ *pDesiredCol = *pCol;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+Left -- word left
+ if (key == (0x73 | 0x100)) {
+ SEL_BEGIN();
+ int32_t off = CUR_OFF();
+ int32_t newOff = wordBoundaryLeft(buf, off);
+ FROM_OFF(newOff);
+ *pDesiredCol = *pCol;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+Right -- word right
+ if (key == (0x74 | 0x100)) {
+ SEL_BEGIN();
+ int32_t off = CUR_OFF();
+ int32_t newOff = wordBoundaryRight(buf, *pLen, off);
+ FROM_OFF(newOff);
+ *pDesiredCol = *pCol;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Up arrow
+ if (key == KEY_UP) {
+ SEL_BEGIN();
+
+ if (*pRow > 0) {
+ (*pRow)--;
+ int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
+ *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
+ }
+
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Down arrow
+ if (key == KEY_DOWN) {
+ SEL_BEGIN();
+
+ if (*pRow < totalLines - 1) {
+ (*pRow)++;
+ int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
+ *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
+ }
+
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Home
+ if (key == KEY_HOME) {
+ SEL_BEGIN();
+ *pCol = 0;
+ *pDesiredCol = 0;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // End
+ if (key == KEY_END) {
+ SEL_BEGIN();
+ *pCol = textEditLineLen(lc, buf, *pLen, *pRow);
+ *pDesiredCol = *pCol;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Page Up
+ if (key == KEY_PGUP) {
+ SEL_BEGIN();
+ *pRow -= visRows;
+
+ if (*pRow < 0) {
+ *pRow = 0;
+ }
+
+ int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
+ *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Page Down
+ if (key == KEY_PGDN) {
+ SEL_BEGIN();
+ *pRow += visRows;
+
+ if (*pRow >= totalLines) {
+ *pRow = totalLines - 1;
+ }
+
+ int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
+ *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+Home
+ if (key == (0x77 | 0x100)) {
+ SEL_BEGIN();
+ *pRow = 0;
+ *pCol = 0;
+ *pDesiredCol = 0;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ // Ctrl+End
+ if (key == (0x75 | 0x100)) {
+ SEL_BEGIN();
+ FROM_OFF(*pLen);
+ *pDesiredCol = *pCol;
+ SEL_END();
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+}
+
+ // Tab key
+ if (key == 9 && opts->captureTabs && !opts->readOnly) {
+ if (*pLen < bufSize - 1) {
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+
+ if (HAS_SEL()) {
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ }
+
+ int32_t off = CUR_OFF();
+
+ if (opts->useTabChar) {
+ if (*pLen < bufSize - 1) {
+ memmove(buf + off + 1, buf + off, *pLen - off + 1);
+ buf[off] = '\t';
+ (*pLen)++;
+ (*pCol)++;
+ *pDesiredCol = *pCol;
+ }
+ } else {
+ int32_t spaces = tabW - (*pCol % tabW);
+
+ for (int32_t s = 0; s < spaces && *pLen < bufSize - 1; s++) {
+ memmove(buf + off + 1, buf + off, *pLen - off + 1);
+ buf[off] = ' ';
+ off++;
+ (*pLen)++;
+ (*pCol)++;
+ }
+
+ *pDesiredCol = *pCol;
+ }
+
+ textEditLineCacheDirty(lc);
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ }
+
+ return;
+ }
+
+ // Printable character
+ if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && !opts->readOnly) {
+ if (*pLen < bufSize - 1) {
+ textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
+
+ if (HAS_SEL()) {
+ int32_t lo = SEL_LO();
+ int32_t hi = SEL_HI();
+ memmove(buf + lo, buf + hi, *pLen - hi + 1);
+ *pLen -= (hi - lo);
+ FROM_OFF(lo);
+ *pSA = -1;
+ *pSC = -1;
+ }
+
+ int32_t off = CUR_OFF();
+
+ if (*pLen < bufSize - 1) {
+ memmove(buf + off + 1, buf + off, *pLen - off + 1);
+ buf[off] = (char)key;
+ (*pLen)++;
+ (*pCol)++;
+ *pDesiredCol = *pCol;
+ textEditLineCacheNotifyInsert(lc, buf, *pLen, off, 1);
+ } else {
+ textEditLineCacheDirty(lc);
+ }
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
+ }
+
+ ENSURE_VIS();
+ wgtInvalidatePaint(w);
+ return;
+ }
+
+ #undef CUR_OFF
+ #undef ENSURE_VIS
+ #undef FROM_OFF
+ #undef SEL_BEGIN
+ #undef SEL_END
+ #undef HAS_SEL
+ #undef SEL_LO
+ #undef SEL_HI
+}
+
+
+// Paints the text content of a multi-line editor inside the box at
+// (textX, textY, innerW, visRows*charHeight). Handles tab expansion,
+// optional per-character syntax coloring, selection highlighting,
+// past-EOL selection fill, cursor draw, and optional per-line
+// background overrides via hooks->lineDecorator.
+//
+// Does NOT touch the surrounding border, gutter, scrollbars, or
+// focus rect -- those are caller concerns (widget chrome).
+//
+// Selection offsets are byte offsets into buf. selAnchor/selCursor
+// may be -1 to indicate no selection; unequal values with both >= 0
+// produce a highlighted range.
+void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks) {
+ int32_t totalLines = textEditLineCount(lc, buf, len);
+ int32_t selLo = -1;
+ int32_t selHi = -1;
+
+ if (selAnchor >= 0 && selCursor >= 0 && selAnchor != selCursor) {
+ selLo = selAnchor < selCursor ? selAnchor : selCursor;
+ selHi = selAnchor < selCursor ? selCursor : selAnchor;
+ }
+
+ int32_t lineOff = textEditLineStart(lc, buf, len, scrollRow);
+ int32_t scratchLen = hooks ? hooks->scratchLen : 0;
+ uint8_t *rawSyntax = hooks ? hooks->rawSyntax : NULL;
+ char *expandBuf = hooks ? hooks->expandBuf : NULL;
+ uint8_t *syntaxBuf = hooks ? hooks->syntaxBuf : NULL;
+
+ for (int32_t i = 0; i < visRows; i++) {
+ int32_t row = scrollRow + i;
+
+ if (row >= totalLines) {
+ break;
+ }
+
+ int32_t lineL = textEditLineLen(lc, buf, len, row);
+ int32_t drawY = textY + i * font->charHeight;
+
+ uint32_t lineBg = bg;
+ uint32_t gutterColor = 0;
+
+ if (hooks && hooks->lineDecorator) {
+ uint32_t decBg = hooks->lineDecorator(row + 1, &gutterColor, hooks->lineDecoratorCtx);
+
+ if (decBg) {
+ lineBg = decBg;
+ }
+
+ rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg);
+ }
+
+ uint32_t savedBg = bg;
+ bg = lineBg;
+
+ bool hasSyntax = false;
+
+ if (hooks && hooks->colorize && lineL > 0 && rawSyntax && scratchLen > 0) {
+ int32_t colorLen = lineL < scratchLen ? lineL : scratchLen;
+ memset(rawSyntax, 0, colorLen);
+ hooks->colorize(buf + lineOff, colorLen, rawSyntax, hooks->colorizeCtx);
+ hasSyntax = true;
+ }
+
+ int32_t expandLen = 0;
+ int32_t vc = 0;
+
+ if (expandBuf && syntaxBuf && scratchLen > 0) {
+ for (int32_t j = 0; j < lineL && expandLen < scratchLen - tabW; j++) {
+ if (buf[lineOff + j] == '\t') {
+ int32_t spaces = tabW - (vc % tabW);
+ uint8_t sc = hasSyntax && j < scratchLen ? rawSyntax[j] : 0;
+
+ for (int32_t s = 0; s < spaces && expandLen < scratchLen; s++) {
+ expandBuf[expandLen] = ' ';
+ syntaxBuf[expandLen] = sc;
+ expandLen++;
+ vc++;
+ }
+ } else {
+ expandBuf[expandLen] = buf[lineOff + j];
+ syntaxBuf[expandLen] = hasSyntax && j < scratchLen ? rawSyntax[j] : 0;
+ expandLen++;
+ vc++;
+ }
+ }
+ }
+
+ int32_t visStart = scrollCol;
+ int32_t visEnd = scrollCol + visCols;
+ int32_t textEnd = expandLen;
+ int32_t drawStart = visStart < textEnd ? visStart : textEnd;
+ int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd;
+
+ int32_t lineSelLo = -1;
+ int32_t lineSelHi = -1;
+
+ if (selLo >= 0) {
+ if (selLo < lineOff + lineL + 1 && selHi > lineOff) {
+ int32_t byteSelLo = selLo - lineOff;
+ int32_t byteSelHi = selHi - lineOff;
+
+ if (byteSelLo < 0) {
+ byteSelLo = 0;
+ }
+
+ lineSelLo = textEditVisualCol(buf, lineOff, lineOff + byteSelLo, tabW);
+ lineSelHi = textEditVisualCol(buf, lineOff, lineOff + (byteSelHi < lineL ? byteSelHi : lineL), tabW);
+
+ if (byteSelHi > lineL) {
+ lineSelHi = expandLen + 1;
+ }
+ }
+ }
+
+ const uint32_t *custom = hooks ? hooks->customSyntaxColors : NULL;
+
+ if (lineSelLo >= 0 && lineSelLo < lineSelHi) {
+ int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo;
+ int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd;
+
+ if (vSelLo > vSelHi) {
+ vSelLo = vSelHi;
+ }
+
+ if (drawStart < vSelLo) {
+ if (hasSyntax) {
+ textEditDrawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, custom);
+ } else {
+ drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true);
+ }
+ }
+
+ if (vSelLo < vSelHi) {
+ drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, expandBuf + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
+ }
+
+ if (vSelHi < drawEnd) {
+ if (hasSyntax) {
+ textEditDrawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, custom);
+ } else {
+ drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true);
+ }
+ }
+
+ int32_t nlOff = lineOff + lineL;
+ bool pastEolSelected = (nlOff >= selLo && nlOff < selHi);
+
+ if (pastEolSelected && drawEnd < visEnd) {
+ int32_t selPastStart = drawEnd < lineSelLo ? lineSelLo : drawEnd;
+ int32_t selPastEnd = visEnd;
+
+ if (selPastStart < visStart) {
+ selPastStart = visStart;
+ }
+
+ if (selPastStart < selPastEnd) {
+ rectFill(d, ops, textX + (selPastStart - scrollCol) * font->charWidth, drawY, (selPastEnd - selPastStart) * font->charWidth, font->charHeight, colors->menuHighlightBg);
+ }
+ }
+ } else {
+ if (drawStart < drawEnd) {
+ if (hasSyntax) {
+ textEditDrawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, custom);
+ } else {
+ drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true);
+ }
+ }
+ }
+
+ bg = savedBg;
+
+ lineOff += lineL;
+
+ if (lineOff < len && buf[lineOff] == '\n') {
+ lineOff++;
+ }
+ }
+
+ if (showCursor) {
+ int32_t curLineOff = textEditLineStart(lc, buf, len, cursorRow);
+ int32_t curOff = curLineOff + cursorCol;
+
+ if (curOff > len) {
+ curOff = len;
+ }
+
+ int32_t curVisCol = textEditVisualCol(buf, curLineOff, curOff, tabW);
+ int32_t curDrawCol = curVisCol - scrollCol;
+ int32_t curDrawRow = cursorRow - scrollRow;
+
+ if (curDrawCol >= 0 && curDrawCol <= visCols && curDrawRow >= 0 && curDrawRow < visRows) {
+ int32_t cursorX = textX + curDrawCol * font->charWidth;
+ int32_t cursorY = textY + curDrawRow * font->charHeight;
+ drawVLine(d, ops, cursorX, cursorY, font->charHeight, fg);
+ }
+ }
+}
+
+
// This is the core single-line text editing engine, parameterized by
// pointer to allow reuse across TextInput, Spinner, and ComboBox.
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth) {
@@ -696,6 +2310,153 @@ void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT
}
+// Paints a single vertical or horizontal text-grid scrollbar:
+// trough, end-arrow buttons with direction triangles, and thumb.
+// All items are in logical units (lines/cols).
+void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll) {
+ BevelStyleT troughBevel = BEVEL_TROUGH(colors);
+ BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
+
+ if (vertical) {
+ drawBevel(d, ops, x, y, thick, len, &troughBevel);
+ } else {
+ drawBevel(d, ops, x, y, len, thick, &troughBevel);
+ }
+
+ // Starting-end arrow button (up / left)
+ drawBevel(d, ops, x, y, thick, thick, &btnBevel);
+
+ {
+ int32_t cx = x + thick / 2;
+ int32_t cy = y + thick / 2;
+
+ if (vertical) {
+ for (int32_t i = 0; i < 4; i++) {
+ drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg);
+ }
+ } else {
+ for (int32_t i = 0; i < 4; i++) {
+ drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, colors->contentFg);
+ }
+ }
+ }
+
+ // Ending-end arrow button (down / right)
+ int32_t endX = vertical ? x : x + len - thick;
+ int32_t endY = vertical ? y + len - thick : y;
+ drawBevel(d, ops, endX, endY, thick, thick, &btnBevel);
+
+ {
+ int32_t cx = endX + thick / 2;
+ int32_t cy = endY + thick / 2;
+
+ if (vertical) {
+ for (int32_t i = 0; i < 4; i++) {
+ drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg);
+ }
+ } else {
+ for (int32_t i = 0; i < 4; i++) {
+ drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, colors->contentFg);
+ }
+ }
+ }
+
+ int32_t trackLen = len - thick * 2;
+
+ if (trackLen > 0) {
+ int32_t thumbPos;
+ int32_t thumbSize;
+ widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
+
+ if (vertical) {
+ drawBevel(d, ops, x, y + thick + thumbPos, thick, thumbSize, &btnBevel);
+ } else {
+ drawBevel(d, ops, x + thick + thumbPos, y, thumbSize, thick, &btnBevel);
+ }
+ }
+}
+
+
+// Converts a mouse coordinate (during an active thumb drag) to a
+// clamped scroll value. dragOff is the thumb-relative offset captured
+// when the drag started (from the hit-test's *pDragOff).
+int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) {
+ (void)vertical;
+ int32_t maxScroll = total - visible;
+
+ if (maxScroll <= 0) {
+ return 0;
+ }
+
+ int32_t trackLen = len - thick * 2;
+ int32_t thumbPos;
+ int32_t thumbSize;
+ widgetScrollbarThumb(trackLen, total, visible, 0, &thumbPos, &thumbSize);
+
+ int32_t rel = mouseCoord - sbStart - thick - dragOff;
+ int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * rel) / (trackLen - thumbSize) : 0;
+
+ if (newScroll < 0) {
+ newScroll = 0;
+ }
+
+ if (newScroll > maxScroll) {
+ newScroll = maxScroll;
+ }
+
+ return newScroll;
+}
+
+
+// Classifies a mouse click against a text-grid scrollbar. Returns
+// one of the TEXT_SB_HIT_* codes. Callers typically translate UP/
+// DOWN to single-step scrolls and PAGE_UP/PAGE_DOWN to page-sized
+// jumps. THUMB begins a drag; *pDragOff captures the click offset
+// relative to the thumb origin for use in subsequent drag updates.
+int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff) {
+ int32_t primary = vertical ? (vy - y) : (vx - x);
+ int32_t cross = vertical ? (vx - x) : (vy - y);
+
+ if (cross < 0 || cross >= thick || primary < 0 || primary >= len) {
+ return TEXT_SB_HIT_NONE;
+ }
+
+ if (primary < thick) {
+ return TEXT_SB_HIT_UP;
+ }
+
+ if (primary >= len - thick) {
+ return TEXT_SB_HIT_DOWN;
+ }
+
+ int32_t trackLen = len - thick * 2;
+
+ if (trackLen <= 0) {
+ return TEXT_SB_HIT_NONE;
+ }
+
+ int32_t thumbPos;
+ int32_t thumbSize;
+ widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
+
+ int32_t trackRel = primary - thick;
+
+ if (trackRel < thumbPos) {
+ return TEXT_SB_HIT_PAGE_UP;
+ }
+
+ if (trackRel >= thumbPos + thumbSize) {
+ return TEXT_SB_HIT_PAGE_DOWN;
+ }
+
+ if (pDragOff) {
+ *pDragOff = trackRel - thumbPos;
+ }
+
+ return TEXT_SB_HIT_THUMB;
+}
+
+
int32_t wordBoundaryLeft(const char *buf, int32_t pos) {
if (pos <= 0) {
return 0;
diff --git a/src/libs/kpunch/texthelp/textHelp.h b/src/libs/kpunch/texthelp/textHelp.h
index 35e473a..5f6d2da 100644
--- a/src/libs/kpunch/texthelp/textHelp.h
+++ b/src/libs/kpunch/texthelp/textHelp.h
@@ -51,6 +51,8 @@ void clearOtherSelections(WidgetT *except);
// ============================================================
bool isWordChar(char c);
+int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
+int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
int32_t wordBoundaryLeft(const char *buf, int32_t pos);
int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
int32_t wordEnd(const char *buf, int32_t len, int32_t pos);
@@ -62,6 +64,240 @@ int32_t wordStart(const char *buf, int32_t pos);
void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
+// ============================================================
+// Multi-line line-offset cache
+// ============================================================
+//
+// Library-owned cache of line-start offsets and tab-expanded visual
+// lengths for a text buffer. Widget owns the backing buffer; the
+// cache is a derived index that must be invalidated (dirty) or
+// incrementally updated whenever the buffer mutates.
+//
+// Contract: every mutator call on the cache takes the current buf
+// and len so any fallback-to-rebuild path has what it needs. If the
+// cache's tabWidth is changed via textEditLineCacheSetTabWidth it
+// automatically dirties itself, since visual-length entries depend
+// on tab stops.
+typedef struct TextEditLineCacheT {
+ int32_t *lineOffsets; // [lineCount+1], last entry = buffer length sentinel
+ int32_t *lineVisLens; // [lineCount], tab-expanded visual column counts
+ int32_t lineOffsetCap;
+ int32_t lineVisLenCap;
+ int32_t cachedLines; // -1 = dirty, otherwise >= 1
+ int32_t cachedMaxLL; // -1 = unknown
+ int32_t tabWidth; // tab stop width used to compute lineVisLens
+} TextEditLineCacheT;
+
+void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth);
+void textEditLineCacheFree(TextEditLineCacheT *lc);
+void textEditLineCacheDirty(TextEditLineCacheT *lc);
+void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
+void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len);
+void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen);
+void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen);
+int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len);
+int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
+int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
+int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len);
+void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col);
+int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col);
+
+// ============================================================
+// Syntax coloring palette
+// ============================================================
+//
+// Indices returned by a widget-supplied colorize callback. The palette
+// has hard-coded defaults; any non-zero entry in a caller-supplied
+// customColors[TEXT_SYNTAX_MAX] overrides the default.
+
+#define TEXT_SYNTAX_DEFAULT 0
+#define TEXT_SYNTAX_KEYWORD 1
+#define TEXT_SYNTAX_STRING 2
+#define TEXT_SYNTAX_COMMENT 3
+#define TEXT_SYNTAX_NUMBER 4
+#define TEXT_SYNTAX_OPERATOR 5
+#define TEXT_SYNTAX_TYPE 6
+#define TEXT_SYNTAX_MAX 7
+
+uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
+void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
+
+// ============================================================
+// Scroll adjustment
+// ============================================================
+//
+// Adjust scrollRow/scrollCol so the cursor stays inside the visible
+// window [visRows x visCols]. Visual column accounts for tab width.
+void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol);
+
+// ============================================================
+// Multi-line paint
+// ============================================================
+//
+// Renders the text content of a multi-line editor: per-line tab
+// expansion, optional syntax coloring, selection highlight, cursor,
+// and optional per-line background overrides. All widget chrome
+// (border, gutter, scrollbars, focus rect) stays with the caller.
+//
+// The three scratch buffers (rawSyntax, expandBuf, syntaxBuf) are
+// caller-owned so the caller controls the truncation policy for
+// very long lines. Lines longer than scratchLen are drawn up to
+// that limit and truncated cleanly.
+typedef struct TextEditPaintHooksT {
+ void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
+ void *colorizeCtx;
+ uint32_t (*lineDecorator)(int32_t lineNum, uint32_t *gutterColor, void *ctx);
+ void *lineDecoratorCtx;
+ uint8_t *rawSyntax;
+ char *expandBuf;
+ uint8_t *syntaxBuf;
+ int32_t scratchLen;
+ const uint32_t *customSyntaxColors;
+} TextEditPaintHooksT;
+
+void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks);
+
+// ============================================================
+// Multi-line mouse click and drag
+// ============================================================
+//
+// Mouse handling for the text content area. Caller is responsible
+// for all chrome hit-tests (scrollbar, gutter) before calling.
+//
+// widgetTextEditMultiMouseClick places the cursor at the click
+// position, handling single (cursor + drag anchor), double (select
+// word), and triple (select line) clicks. Caller gets back updated
+// cursor/desired/sel state.
+//
+// widgetTextEditMultiDragUpdateArea is called during a drag to
+// auto-scroll the view and extend the selection.
+void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
+void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor);
+
+// ============================================================
+// Multi-line key handler
+// ============================================================
+//
+// Options bundle: bundles the small set of mode flags the key
+// handler needs to know about. Avoids a 20-argument entry point.
+
+typedef struct TextEditMultiOptionsT {
+ bool autoIndent;
+ bool captureTabs;
+ bool useTabChar;
+ int32_t tabWidth;
+ bool readOnly;
+} TextEditMultiOptionsT;
+
+// Runs a single keystroke through the multi-line editing engine.
+// Handles clipboard (Ctrl+A/C/V/X), undo (Ctrl+Z), navigation
+// (arrows, Ctrl-arrows, Home/End, Ctrl-Home/Ctrl-End, PgUp/PgDn),
+// editing (Enter with auto-indent, Backspace, Delete, Tab,
+// printable characters), and shift-based selection extension.
+//
+// The caller owns buf/undoBuf storage; buf must have room for
+// bufSize - 1 characters plus terminator. The library invalidates
+// the cache when it knows edits span newlines; incremental
+// single-byte updates go through the cache's notify paths.
+//
+// Fires w->onChange for edits that mutate the buffer. Calls
+// wgtInvalidatePaint(w) before returning for any keystroke that
+// visibly changes state.
+void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts);
+
+// ============================================================
+// Text-grid scrollbars
+// ============================================================
+//
+// Scrollbar helpers for widgets that scroll a text grid (lines x
+// columns) rather than pixels. Unlike the general-purpose bevel
+// scrollbars, these pass `total`/`visible`/`scroll` in logical
+// units (lines or columns). Layout is fixed: arrow button at each
+// end sized `thick` x `thick`, remainder is the thumb track.
+//
+// widgetTextScrollbarHitTest returns one of the TEXT_SB_HIT_*
+// values. TEXT_SB_HIT_THUMB sets *pDragOff to the click offset
+// from the thumb origin, for use in subsequent drag updates.
+//
+// widgetTextScrollbarDragToScroll converts a mouse coordinate
+// during an active thumb drag to a clamped scroll value.
+
+#define TEXT_SB_HIT_NONE 0
+#define TEXT_SB_HIT_UP 1
+#define TEXT_SB_HIT_DOWN 2
+#define TEXT_SB_HIT_PAGE_UP 3
+#define TEXT_SB_HIT_PAGE_DOWN 4
+#define TEXT_SB_HIT_THUMB 5
+
+void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
+int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
+int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
+
+// ============================================================
+// Multi-line editor state aggregate
+// ============================================================
+//
+// Groups the per-editor state (buffer, cursor, selection, scroll,
+// undo slot, line cache) into a single struct. Consumers embed this
+// in their widget data; the library operates on the individual
+// fields via pointer parameters (see widgetTextEditMulti* calls).
+//
+// Allocation policy: caller owns the buf/undoBuf storage. Use
+// textEditMultiInit to allocate them (returns false if malloc
+// fails) and textEditMultiFree to release.
+typedef struct TextEditMultiT {
+ char *buf;
+ int32_t bufSize;
+ int32_t len;
+
+ int32_t cursorRow;
+ int32_t cursorCol;
+ int32_t desiredCol;
+
+ int32_t scrollRow;
+ int32_t scrollCol;
+
+ int32_t selAnchor;
+ int32_t selCursor;
+
+ char *undoBuf;
+ int32_t undoLen;
+ int32_t undoCursor;
+
+ TextEditLineCacheT lines;
+} TextEditMultiT;
+
+bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
+void textEditMultiFree(TextEditMultiT *te);
+
+// ============================================================
+// Multi-line text operations
+// ============================================================
+//
+// Pure text operations: no widget chrome, no scrollbar math (beyond
+// visRows for centering). The caller invalidates paint / ensures
+// visibility using its own conventions after these return.
+//
+// textEditFindNext: searches forward/backward from the current
+// cursor position. If found, selects the match and moves the cursor
+// to the match's start (forward) or end (backward). Returns true
+// on match, false on no-match (no wrap-around).
+//
+// textEditReplaceAll: replaces every occurrence of needle with
+// replacement. Records a single undo snapshot before any change.
+// Returns the number of replacements made.
+//
+// textEditGoToLine: places cursor at the start of the given
+// 1-based line, selects the entire line, and scrolls so the line
+// sits ~1/4 from the top of the visible area (with clamping).
+//
+// textEditGetWordAtCursor: writes the word under the cursor into
+// the caller's buffer. Returns the length written (0 if no word).
+bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
+int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize);
+void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor);
+int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
+
// ============================================================
// Single-line text editing engine
// ============================================================
diff --git a/src/tools/dvxhlpc.c b/src/tools/dvxhlpc.c
index 595fc0b..1394b6b 100644
--- a/src/tools/dvxhlpc.c
+++ b/src/tools/dvxhlpc.c
@@ -689,6 +689,12 @@ int32_t hlpcCompile(const char **inputFiles, int32_t inputCount, const char *out
pass2Wrap();
progressStep();
+ // Sort index entries alphabetically. Done once here so both the
+ // HTML sidebar and the binary index section see the same order.
+ if (indexCount > 1) {
+ qsort(indexEntries, indexCount, sizeof(IndexEntryT), compareIndexEntries);
+ }
+
// HTML output (uses wrapped text, before binary passes)
if (htmlPath) {
if (emitHtml(htmlPath) == 0) {
@@ -1523,8 +1529,7 @@ static int pass5Serialize(const char *outputPath) {
offset += sizeof(entry);
}
- // --- 4. Keyword index entries (sorted) ---
- qsort(indexEntries, indexCount, sizeof(IndexEntryT), compareIndexEntries);
+ // --- 4. Keyword index entries (sorted in compile() after pass2) ---
hdr.indexOffset = offset;
hdr.indexCount = indexCount;
for (int32_t i = 0; i < indexCount; i++) {
diff --git a/src/widgets/kpunch/Makefile b/src/widgets/kpunch/Makefile
index de91126..78938fc 100644
--- a/src/widgets/kpunch/Makefile
+++ b/src/widgets/kpunch/Makefile
@@ -96,7 +96,7 @@ define WIDGET_RULES
$(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subst :, ,$1)).c $$(WIDGET_DEPS_H) | $(OBJDIR)
$$(CC) $$(CFLAGS) -c -o $$@ $$<
-$(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(WGTDIR)/$(word 1,$(subst :, ,$1))
+$(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o $$(wildcard $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res) $$(wildcard $(word 2,$(subst :, ,$1))/*.bmp) | $(WGTDIR)/$(word 1,$(subst :, ,$1))
$$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe -U $$<
mv $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe $$@
@if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \
diff --git a/src/widgets/kpunch/canvas/widgetCanvas.c b/src/widgets/kpunch/canvas/widgetCanvas.c
index 7c12bb4..94d8263 100644
--- a/src/widgets/kpunch/canvas/widgetCanvas.c
+++ b/src/widgets/kpunch/canvas/widgetCanvas.c
@@ -1045,7 +1045,11 @@ static const WgtMethodDescT sMethods[] = {
};
static const WgtIfaceT sIface = {
- .basName = "Canvas",
+ // VB-compatible name is "PictureBox" -- matches what the rest of
+ // the docs (ideguide, canvas.bhs) and sample .frm files (iconed)
+ // already use. Prior "Canvas" basName caused silent frm-load
+ // drops whenever an app asked for a PictureBox.
+ .basName = "PictureBox",
.props = NULL,
.propCount = 0,
.methods = sMethods,
diff --git a/src/widgets/kpunch/checkbox/widgetCheckbox.c b/src/widgets/kpunch/checkbox/widgetCheckbox.c
index 157675a..e490354 100644
--- a/src/widgets/kpunch/checkbox/widgetCheckbox.c
+++ b/src/widgets/kpunch/checkbox/widgetCheckbox.c
@@ -99,8 +99,13 @@ void widgetCheckboxAccelActivate(WidgetT *w, WidgetT *root) {
// relative to each other regardless of font size.
void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
CheckboxDataT *d = (CheckboxDataT *)w->data;
+ // +1 on the width: widgetCheckboxPaint draws the focus rect from
+ // labelX-1 with width labelW+2, whose rightmost column lands one
+ // pixel past the widget's claimed right edge. Without the +1,
+ // rectFill in the next paint doesn't clear that column and the
+ // stale dot survives after focus moves away.
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
- textWidthAccel(font, d->text);
+ textWidthAccel(font, d->text) + 1;
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
}
diff --git a/src/widgets/kpunch/dbGrid/dbGrid.h b/src/widgets/kpunch/dbGrid/dbGrid.h
index 1272fb6..9ad4016 100644
--- a/src/widgets/kpunch/dbGrid/dbGrid.h
+++ b/src/widgets/kpunch/dbGrid/dbGrid.h
@@ -35,6 +35,7 @@ typedef struct {
void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
int32_t (*getSelectedRow)(const WidgetT *w);
+ void (*setSelectedRow)(WidgetT *w, int32_t row);
} DbGridApiT;
static inline const DbGridApiT *dvxDbGridApi(void) {
@@ -48,5 +49,6 @@ static inline const DbGridApiT *dvxDbGridApi(void) {
#define wgtDbGridSetColumnHeader(w, field, hdr) dvxDbGridApi()->setColumnHeader(w, field, hdr)
#define wgtDbGridSetColumnWidth(w, field, width) dvxDbGridApi()->setColumnWidth(w, field, width)
#define wgtDbGridGetSelectedRow(w) dvxDbGridApi()->getSelectedRow(w)
+#define wgtDbGridSetSelectedRow(w, row) dvxDbGridApi()->setSelectedRow(w, row)
#endif // DBGRID_H
diff --git a/src/widgets/kpunch/dbGrid/widgetDbGrid.c b/src/widgets/kpunch/dbGrid/widgetDbGrid.c
index d1eb76e..631d717 100644
--- a/src/widgets/kpunch/dbGrid/widgetDbGrid.c
+++ b/src/widgets/kpunch/dbGrid/widgetDbGrid.c
@@ -145,6 +145,7 @@ static void dbGridDestroy(WidgetT *w);
static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my);
static bool dbGridGetGridLines(const WidgetT *w);
int32_t dbGridGetSelectedRow(const WidgetT *w);
+void dbGridSetSelectedRow(WidgetT *w, int32_t row);
static void dbGridOnDragEnd(WidgetT *w);
static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod);
@@ -384,6 +385,40 @@ int32_t dbGridGetSelectedRow(const WidgetT *w) {
}
+// Programmatically select a data-row (same effect as a user click).
+// Clamps to the current row range, syncs the bound Data control's
+// cursor (which cascades to any controls bound to that Data via
+// the normal Reposition path), and repaints. -1 deselects.
+void dbGridSetSelectedRow(WidgetT *w, int32_t row) {
+ if (!w || !w->data) {
+ return;
+ }
+
+ DbGridDataT *d = (DbGridDataT *)w->data;
+ int32_t rowCount = getDataRowCount(d);
+
+ if (row < -1) {
+ row = -1;
+ }
+
+ if (row >= rowCount) {
+ row = rowCount - 1;
+ }
+
+ if (d->selectedRow == row) {
+ return;
+ }
+
+ d->selectedRow = row;
+
+ if (d->dataWidget && row >= 0) {
+ wgtDataCtrlSetCurrentRow(d->dataWidget, row);
+ }
+
+ wgtInvalidatePaint(w);
+}
+
+
static void dbGridOnDragEnd(WidgetT *w) {
DbGridDataT *d = (DbGridDataT *)w->data;
d->sbDragOrient = -2;
@@ -1191,6 +1226,7 @@ static const struct {
void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
int32_t (*getSelectedRow)(const WidgetT *w);
+ void (*setSelectedRow)(WidgetT *w, int32_t row);
} sApi = {
.create = dbGridCreate,
.setDataWidget = dbGridSetDataWidget,
@@ -1199,14 +1235,19 @@ static const struct {
.setColumnHeader = dbGridSetColumnHeader,
.setColumnWidth = dbGridSetColumnWidth,
.getSelectedRow = dbGridGetSelectedRow,
+ .setSelectedRow = dbGridSetSelectedRow,
};
static const WgtPropDescT sProps[] = {
- { "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL },
+ { "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL },
+ { "SelectedRow", WGT_IFACE_INT, (void *)dbGridGetSelectedRow, (void *)dbGridSetSelectedRow, NULL },
};
static const WgtMethodDescT sMethods[] = {
- { "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh },
+ { "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh },
+ { "SetColumnVisible", WGT_SIG_STR_BOOL, (void *)dbGridSetColumnVisible },
+ { "SetColumnHeader", WGT_SIG_STR_STR, (void *)dbGridSetColumnHeader },
+ { "SetColumnWidth", WGT_SIG_STR_INT, (void *)dbGridSetColumnWidth },
};
static const WgtEventDescT sEvents[] = {
@@ -1217,11 +1258,11 @@ static const WgtEventDescT sEvents[] = {
static const WgtIfaceT sIface = {
.basName = "DBGrid",
.props = sProps,
- .propCount = 1,
+ .propCount = sizeof(sProps) / sizeof(sProps[0]),
.methods = sMethods,
- .methodCount = 1,
+ .methodCount = sizeof(sMethods) / sizeof(sMethods[0]),
.events = sEvents,
- .eventCount = 2,
+ .eventCount = sizeof(sEvents) / sizeof(sEvents[0]),
.createSig = WGT_CREATE_PARENT,
.isContainer = false,
.defaultEvent = "DblClick",
diff --git a/src/widgets/kpunch/radio/radio.h b/src/widgets/kpunch/radio/radio.h
index 8c59773..22f59f0 100644
--- a/src/widgets/kpunch/radio/radio.h
+++ b/src/widgets/kpunch/radio/radio.h
@@ -26,9 +26,14 @@
#include "../../../libs/kpunch/libdvx/dvxWgt.h"
+// Field order mirrors widgetRadio.c's sApi. `create` must be first
+// because the BASIC form runtime invokes widget constructors by
+// dereferencing the api pointer as a pointer-to-function-pointer
+// (see createWidgetByIface). If the order is changed here, update
+// widgetRadio.c to match.
typedef struct {
- WidgetT *(*group)(WidgetT *parent);
WidgetT *(*create)(WidgetT *parent, const char *text);
+ WidgetT *(*group)(WidgetT *parent);
void (*groupSetSelected)(WidgetT *w, int32_t idx);
int32_t (*getIndex)(const WidgetT *w);
} RadioApiT;
diff --git a/src/widgets/kpunch/radio/widgetRadio.c b/src/widgets/kpunch/radio/widgetRadio.c
index 6ed869f..a39c4a7 100644
--- a/src/widgets/kpunch/radio/widgetRadio.c
+++ b/src/widgets/kpunch/radio/widgetRadio.c
@@ -55,6 +55,12 @@ static int32_t sRadioTypeId = -1;
typedef struct {
const char *text;
int32_t index;
+ // `selected` is the source of truth for .frm-loaded OptionButtons,
+ // whose parent is a plain layout container rather than a
+ // RadioGroup. When the parent IS a RadioGroup, the group's
+ // selectedIdx still wins (the group view is canonical and the C
+ // API callers like dvxdemo keep using it).
+ bool selected;
} RadioDataT;
typedef struct {
@@ -229,8 +235,13 @@ void widgetRadioAccelActivate(WidgetT *w, WidgetT *root) {
// column, and the indicator + label layout is visually consistent.
void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
RadioDataT *d = (RadioDataT *)w->data;
+ // +1 on the width: widgetRadioPaint draws the focus rect from
+ // labelX-1 with width labelW+2, whose rightmost column lands at
+ // w->x + (BOX+GAP+labelW). Without the +1 that column is one
+ // pixel past the widget's claimed right edge, so the rectFill
+ // in the next paint doesn't clear it and the focus dot survives.
w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP +
- textWidthAccel(font, d->text);
+ textWidthAccel(font, d->text) + 1;
w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight);
}
@@ -244,72 +255,85 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
//
// Key codes use DOS BIOS scancode convention: high byte 0x01 flag
// ORed with the scancode. 0x50=Down, 0x4D=Right, 0x48=Up, 0x4B=Left.
+// Selects `target` within `parent` -- parent may be a RadioGroup (C
+// API flow, updates group selectedIdx) or a plain layout container
+// (.frm-loaded OptionButton flow, updates each RadioDataT.selected).
+static void selectRadio(WidgetT *parent, WidgetT *target) {
+ if (!parent || !target) {
+ return;
+ }
+
+ RadioDataT *td = (RadioDataT *)target->data;
+
+ if (parent->type == sRadioGroupTypeId) {
+ RadioGroupDataT *gd = (RadioGroupDataT *)parent->data;
+ invalidateOldSelection(parent, gd->selectedIdx);
+ gd->selectedIdx = td ? td->index : 0;
+ } else {
+ for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
+ if (c->type == sRadioTypeId && c != target) {
+ RadioDataT *sd = (RadioDataT *)c->data;
+
+ if (sd && sd->selected) {
+ sd->selected = false;
+ wgtInvalidatePaint(c);
+ }
+ }
+ }
+
+ if (td) {
+ td->selected = true;
+ }
+ }
+
+ if (parent->type == sRadioGroupTypeId && parent->onChange) {
+ parent->onChange(parent);
+ } else if (parent->type != sRadioGroupTypeId && target->onChange) {
+ target->onChange(target);
+ }
+}
+
+
void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
- RadioDataT *rd = (RadioDataT *)w->data;
+
+ if (!w->parent) {
+ return;
+ }
if (key == ' ' || key == 0x0D) {
- // Select this radio
- if (w->parent && w->parent->type == sRadioGroupTypeId) {
- RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
- gd->selectedIdx = rd->index;
-
- if (w->parent->onChange) {
- w->parent->onChange(w->parent);
- }
- }
-
+ selectRadio(w->parent, w);
wgtInvalidatePaint(w);
} else if (key == KEY_DOWN || key == KEY_RIGHT) {
- // Down or Right -- next radio in group
- if (w->parent && w->parent->type == sRadioGroupTypeId) {
- WidgetT *next = NULL;
+ WidgetT *next = NULL;
- for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
- if (s->type == sRadioTypeId && s->visible && s->enabled) {
- next = s;
- break;
- }
- }
-
- if (next) {
- RadioDataT *nd = (RadioDataT *)next->data;
- RadioGroupDataT *gd = (RadioGroupDataT *)next->parent->data;
- invalidateOldSelection(w->parent, gd->selectedIdx);
- sFocusedWidget = next;
- gd->selectedIdx = nd->index;
-
- if (next->parent->onChange) {
- next->parent->onChange(next->parent);
- }
-
- wgtInvalidatePaint(next);
+ for (WidgetT *s = w->nextSibling; s; s = s->nextSibling) {
+ if (s->type == sRadioTypeId && s->visible && s->enabled) {
+ next = s;
+ break;
}
}
+
+ if (next) {
+ wgtInvalidatePaint(w); // erase focus rect on widget we are leaving
+ sFocusedWidget = next;
+ selectRadio(w->parent, next);
+ wgtInvalidatePaint(next);
+ }
} else if (key == KEY_UP || key == KEY_LEFT) {
- // Up or Left -- previous radio in group
- if (w->parent && w->parent->type == sRadioGroupTypeId) {
- WidgetT *prev = NULL;
+ WidgetT *prev = NULL;
- for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
- if (s->type == sRadioTypeId && s->visible && s->enabled) {
- prev = s;
- }
+ for (WidgetT *s = w->parent->firstChild; s && s != w; s = s->nextSibling) {
+ if (s->type == sRadioTypeId && s->visible && s->enabled) {
+ prev = s;
}
+ }
- if (prev) {
- RadioDataT *pd = (RadioDataT *)prev->data;
- RadioGroupDataT *gd = (RadioGroupDataT *)prev->parent->data;
- invalidateOldSelection(w->parent, gd->selectedIdx);
- sFocusedWidget = prev;
- gd->selectedIdx = pd->index;
-
- if (prev->parent->onChange) {
- prev->parent->onChange(prev->parent);
- }
-
- wgtInvalidatePaint(prev);
- }
+ if (prev) {
+ wgtInvalidatePaint(w); // erase focus rect on widget we are leaving
+ sFocusedWidget = prev;
+ selectRadio(w->parent, prev);
+ wgtInvalidatePaint(prev);
}
}
}
@@ -321,8 +345,9 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vy;
sFocusedWidget = w;
+ RadioDataT *rd = (RadioDataT *)w->data;
+
if (w->parent && w->parent->type == sRadioGroupTypeId) {
- RadioDataT *rd = (RadioDataT *)w->data;
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
if (gd->selectedIdx != rd->index) {
@@ -334,7 +359,34 @@ void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
if (w->parent->onChange) {
w->parent->onChange(w->parent);
}
+ } else if (w->parent) {
+ // .frm-loaded OptionButton: no enclosing RadioGroup. Walk
+ // sibling OptionButtons, clear theirs, mark this one selected.
+ for (WidgetT *c = w->parent->firstChild; c; c = c->nextSibling) {
+ if (c->type == sRadioTypeId && c != w) {
+ RadioDataT *sd = (RadioDataT *)c->data;
+
+ if (sd && sd->selected) {
+ sd->selected = false;
+ wgtInvalidatePaint(c);
+ }
+ }
+ }
+
+ if (rd) {
+ rd->selected = true;
+ }
+
+ if (w->onChange) {
+ w->onChange(w);
+ }
}
+
+ // Always repaint the clicked radio: its selection dot or focus rect
+ // may need to appear. The mouse dispatcher invalidates the previous
+ // focus owner, but nothing invalidates the newly-focused widget
+ // unless we do it here.
+ wgtInvalidatePaint(w);
}
@@ -389,19 +441,29 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
drawHLine(d, ops, bx + right, boxY + i, 1, sh);
}
- // Draw filled diamond if selected
+ // Draw filled diamond if selected. Two sources of truth: the
+ // RadioGroup's selectedIdx (when parent is a RadioGroup, via C
+ // API or wgtRadioGroupSetSelected), or this widget's own
+ // rd->selected bit (set by mouse/key handlers when the radio is
+ // just a child of a layout container, as happens for
+ // .frm-loaded OptionButtons).
+ bool isSelected = false;
+
if (w->parent && w->parent->type == sRadioGroupTypeId) {
RadioGroupDataT *gd = (RadioGroupDataT *)w->parent->data;
+ isSelected = (gd->selectedIdx == rd->index);
+ } else {
+ isSelected = rd->selected;
+ }
- if (gd->selectedIdx == rd->index) {
- uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
+ if (isSelected) {
+ uint32_t dotFg = w->enabled ? fg : colors->windowShadow;
- static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
+ static const int32_t dotW[] = {2, 4, 6, 6, 4, 2};
- for (int32_t i = 0; i < 6; i++) {
- int32_t dw = dotW[i];
- drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
- }
+ for (int32_t i = 0; i < 6; i++) {
+ int32_t dw = dotW[i];
+ drawHLine(d, ops, bx + mid - dw / 2, boxY + mid - 3 + i, dw, dotFg);
}
}
diff --git a/src/widgets/kpunch/separator/widgetSeparator.c b/src/widgets/kpunch/separator/widgetSeparator.c
index 0714d33..36207f9 100644
--- a/src/widgets/kpunch/separator/widgetSeparator.c
+++ b/src/widgets/kpunch/separator/widgetSeparator.c
@@ -67,6 +67,15 @@ WidgetT *wgtHSeparator(WidgetT *parent) {
if (w) {
SeparatorDataT *d = calloc(1, sizeof(SeparatorDataT));
+ // Auto-orient to the parent: horizontal container -> vertical
+ // divider, vertical container -> horizontal divider. This
+ // lets a single "Line" basName work correctly in both
+ // toolbars and menus without requiring callers to pick
+ // between HSeparator and VSeparator by widget type.
+ if (parent && widgetIsHorizContainer(parent->type)) {
+ d->vertical = true;
+ }
+
w->data = d;
}
diff --git a/src/widgets/kpunch/statusBar/widgetStatusBar.c b/src/widgets/kpunch/statusBar/widgetStatusBar.c
index 50d917b..47aa065 100644
--- a/src/widgets/kpunch/statusBar/widgetStatusBar.c
+++ b/src/widgets/kpunch/statusBar/widgetStatusBar.c
@@ -41,7 +41,11 @@
#include "dvxWgtP.h"
-#define TOOLBAR_PAD 2
+// Tight padding keeps the status bar visually a single-line strip
+// (VB-style). The per-child sunken bevel already adds 1px of visual
+// inset around each panel, so additional container padding just makes
+// the bar look chunky.
+#define TOOLBAR_PAD 1
#define TOOLBAR_GAP 2
static int32_t sTypeId = -1;
@@ -87,7 +91,8 @@ static const WgtIfaceT sIface = {
.methodCount = 0,
.events = NULL,
.eventCount = 0,
- .createSig = WGT_CREATE_PARENT
+ .createSig = WGT_CREATE_PARENT,
+ .isContainer = true
};
void wgtRegister(void) {
diff --git a/src/widgets/kpunch/tabControl/tabctrl.res b/src/widgets/kpunch/tabControl/tabctrl.res
index 6550a00..52f4b9b 100644
--- a/src/widgets/kpunch/tabControl/tabctrl.res
+++ b/src/widgets/kpunch/tabControl/tabctrl.res
@@ -1,5 +1,7 @@
icon24 icon tabctrl.bmp
-name text "TabControl"
+name text "TabStrip"
+icon24-2 icon tabctrl.bmp
+name-2 text "TabPage"
author text "Scott Duensing"
copyright text "Copyright 2026 Scott Duensing"
publisher text "Kangaroo Punch Studios"
diff --git a/src/widgets/kpunch/textInput/widgetTextInput.c b/src/widgets/kpunch/textInput/widgetTextInput.c
index b6805de..fa53c88 100644
--- a/src/widgets/kpunch/textInput/widgetTextInput.c
+++ b/src/widgets/kpunch/textInput/widgetTextInput.c
@@ -83,16 +83,6 @@
static int32_t sTextInputTypeId = -1;
static int32_t sTextAreaTypeId = -1;
-// Syntax color indices (returned by colorize callback)
-#define SYNTAX_DEFAULT 0
-#define SYNTAX_KEYWORD 1
-#define SYNTAX_STRING 2
-#define SYNTAX_COMMENT 3
-#define SYNTAX_NUMBER 4
-#define SYNTAX_OPERATOR 5
-#define SYNTAX_TYPE 6
-#define SYNTAX_MAX 7
-
typedef enum {
InputNormalE,
InputPasswordE,
@@ -115,34 +105,9 @@ typedef struct {
} TextInputDataT;
typedef struct {
- char *buf;
- int32_t bufSize;
- int32_t len;
- int32_t cursorRow;
- int32_t cursorCol;
- int32_t scrollRow;
- int32_t scrollCol;
- int32_t desiredCol;
- int32_t selAnchor;
- int32_t selCursor;
- char *undoBuf;
- int32_t undoLen;
- int32_t undoCursor;
- int32_t cachedLines;
- int32_t cachedMaxLL;
-
- // Line offset cache: lineOffsets[i] = byte offset of start of line i.
- // lineOffsets[lineCount] = past-end sentinel (len or len+1).
- // Rebuilt lazily when cachedLines == -1.
- int32_t *lineOffsets;
- int32_t lineOffsetCap;
-
- // Per-line visual length cache (tab-expanded). -1 = dirty.
- int32_t *lineVisLens;
- int32_t lineVisLenCap;
-
- // Cached cursor byte offset (avoids O(N) recomputation per keystroke)
- int32_t cursorOff;
+ // Core multi-line editor state (buffer, cursor, selection, scroll,
+ // undo, line cache) is owned by textHelp via TextEditMultiT.
+ TextEditMultiT te;
int32_t sbDragOrient;
int32_t sbDragOff;
@@ -170,7 +135,7 @@ typedef struct {
void (*onGutterClick)(WidgetT *w, int32_t lineNum);
// Custom syntax colors (0x00RRGGBB; 0 = use default hardcoded color)
- uint32_t customSyntaxColors[SYNTAX_MAX];
+ uint32_t customSyntaxColors[TEXT_SYNTAX_MAX];
// Pre-allocated paint buffers (avoid 3KB stack alloc per visible line per frame)
uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN)
@@ -192,10 +157,6 @@ typedef struct {
#define TEXTAREA_MIN_ROWS 4
#define TEXTAREA_MIN_COLS 20
-// Slack added to lineOffsets/lineVisLens capacity when growing. Bigger
-// values = fewer reallocs, smaller = less wasted space. 256 empirically
-// keeps realloc traffic low on edits to medium-sized files.
-#define TEXTAREA_LINE_CAP_GROWTH 256
#define MAX_COLORIZE_LEN 1024
// Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c)
#define CURSOR_BLINK_MS 250
@@ -211,32 +172,17 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
static const char *basGetWordAtCursor(const WidgetT *w);
static void basSetSyntaxMode(WidgetT *w, const char *mode);
static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
-static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors);
static bool maskCharValid(char slot, char ch);
static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod);
static int32_t maskFirstSlot(const char *mask);
static bool maskIsSlot(char ch);
static int32_t maskNextSlot(const char *mask, int32_t pos);
static int32_t maskPrevSlot(const char *mask, int32_t pos);
-static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
-static void textAreaCacheDelete(WidgetT *w, int32_t off, int32_t deleteLen);
-static void textAreaCacheInsert(WidgetT *w, int32_t off, int32_t insertLen);
-static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col);
static inline void textAreaDirtyCache(WidgetT *w);
-static void textAreaEnsureCache(WidgetT *w);
static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols);
static int32_t textAreaGetLineCount(WidgetT *w);
static int32_t textAreaGetMaxLineLen(WidgetT *w);
static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font);
-static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row);
-static int32_t textAreaLineLenCached(WidgetT *w, int32_t row);
-static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row);
-static int32_t textAreaLineStartCached(WidgetT *w, int32_t row);
-static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col);
-static void textAreaOffToRowColFast(TextAreaDataT *ta, int32_t off, int32_t *row, int32_t *col);
-static void textAreaRebuildCache(WidgetT *w);
-static int32_t visualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
-static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask);
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen);
@@ -317,7 +263,7 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
// Comment
if (ch == '\'') {
while (i < lineLen) {
- colors[i++] = SYNTAX_COMMENT;
+ colors[i++] = TEXT_SYNTAX_COMMENT;
}
return;
@@ -325,14 +271,14 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
// String literal
if (ch == '"') {
- colors[i++] = SYNTAX_STRING;
+ colors[i++] = TEXT_SYNTAX_STRING;
while (i < lineLen && line[i] != '"') {
- colors[i++] = SYNTAX_STRING;
+ colors[i++] = TEXT_SYNTAX_STRING;
}
if (i < lineLen) {
- colors[i++] = SYNTAX_STRING;
+ colors[i++] = TEXT_SYNTAX_STRING;
}
continue;
@@ -341,7 +287,7 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
// Number
if (isdigit((unsigned char)ch) || (ch == '&' && i + 1 < lineLen && (line[i + 1] == 'H' || line[i + 1] == 'h'))) {
while (i < lineLen && (isxdigit((unsigned char)line[i]) || line[i] == '.' || line[i] == '&' || line[i] == 'H' || line[i] == 'h')) {
- colors[i++] = SYNTAX_NUMBER;
+ colors[i++] = TEXT_SYNTAX_NUMBER;
}
continue;
@@ -360,7 +306,7 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
// Check for REM
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] = SYNTAX_COMMENT;
+ colors[j] = TEXT_SYNTAX_COMMENT;
}
return;
@@ -382,19 +328,19 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
upper[uLen] = '\0';
// Check keywords
- uint8_t c = SYNTAX_DEFAULT;
+ uint8_t c = TEXT_SYNTAX_DEFAULT;
for (int32_t j = 0; sBasKeywords[j]; j++) {
if (strcmp(upper, sBasKeywords[j]) == 0) {
- c = SYNTAX_KEYWORD;
+ c = TEXT_SYNTAX_KEYWORD;
break;
}
}
- if (c == SYNTAX_DEFAULT) {
+ if (c == TEXT_SYNTAX_DEFAULT) {
for (int32_t j = 0; sBasTypes[j]; j++) {
if (strcmp(upper, sBasTypes[j]) == 0) {
- c = SYNTAX_TYPE;
+ c = TEXT_SYNTAX_TYPE;
break;
}
}
@@ -409,11 +355,11 @@ static void basColorize(const char *line, int32_t lineLen, uint8_t *colors, void
// Operator
if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\') {
- colors[i++] = SYNTAX_OPERATOR;
+ colors[i++] = TEXT_SYNTAX_OPERATOR;
continue;
}
- colors[i++] = SYNTAX_DEFAULT;
+ colors[i++] = TEXT_SYNTAX_DEFAULT;
}
}
@@ -544,26 +490,6 @@ static void dhsColorize(const char *line, int32_t lineLen, uint8_t *colors, void
}
-// Draw text with per-character syntax coloring. Batches consecutive
-// characters of the same color into single drawTextN calls.
-static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors) {
- int32_t runStart = 0;
-
- while (runStart < len) {
- uint8_t curColor = syntaxColors[textOff + runStart];
- int32_t runEnd = runStart + 1;
-
- while (runEnd < len && syntaxColors[textOff + runEnd] == curColor) {
- runEnd++;
- }
-
- uint32_t fg = syntaxColor(d, curColor, defaultFg, customColors);
- drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true);
- runStart = runEnd;
- }
-}
-
-
static bool maskCharValid(char slot, char ch) {
switch (slot) {
case '#':
@@ -912,243 +838,28 @@ static int32_t maskPrevSlot(const char *mask, int32_t pos) {
}
-static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) {
- if (idx != SYNTAX_DEFAULT && idx < SYNTAX_MAX && custom && custom[idx]) {
- uint32_t c = custom[idx];
- return packColor(d, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
- }
-
- switch (idx) {
- case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128);
- case SYNTAX_STRING: return packColor(d, 128, 0, 0);
- case SYNTAX_COMMENT: return packColor(d, 0, 128, 0);
- case SYNTAX_NUMBER: return packColor(d, 128, 0, 128);
- case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0);
- case SYNTAX_TYPE: return packColor(d, 0, 128, 128);
- default: return defaultFg;
- }
-}
-
-
-// Incrementally update the cache after deleting bytes at `off`.
-// If the deletion spans newlines, falls back to a full rebuild.
-static void textAreaCacheDelete(WidgetT *w, int32_t off, int32_t deleteLen) {
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
-
- if (ta->cachedLines < 0 || deleteLen <= 0) {
- textAreaDirtyCache(w);
- return;
- }
-
- // Check if deleted bytes contained newlines — check the buffer BEFORE deletion
- // Since deletion already happened, we can't check. Fall back to dirty for safety
- // unless we know it was a single non-newline character.
- if (deleteLen > 1) {
- textAreaDirtyCache(w);
- return;
- }
-
- // Single character delete — check if a line was removed by seeing if
- // line count decreased. Quick check: the character that WAS at `off`
- // is now gone. If the current char at `off` merged two lines, rebuild.
- // Simple heuristic: if the line count from offsets doesn't match after
- // adjusting, rebuild.
-
- // Find which line the deletion was on
- int32_t line = 0;
- for (line = ta->cachedLines - 1; line > 0; line--) {
- if (ta->lineOffsets[line] <= off) {
- break;
- }
- }
-
- // Shift subsequent offsets
- for (int32_t i = line + 1; i <= ta->cachedLines; i++) {
- ta->lineOffsets[i] -= deleteLen;
- }
-
- // Verify the line structure is still valid
- int32_t lineOff = ta->lineOffsets[line];
- int32_t nextOff = (line + 1 <= ta->cachedLines) ? ta->lineOffsets[line + 1] : ta->len;
-
- // If the next line's offset is now <= this line's, a newline was deleted
- if (line + 1 <= ta->cachedLines && nextOff <= lineOff) {
- textAreaDirtyCache(w);
- return;
- }
-
- // Check that no newline appears before the expected boundary
- const char *buf = ta->buf;
- for (int32_t i = lineOff; i < nextOff && i < ta->len; i++) {
- if (buf[i] == '\n') {
- if (i < nextOff - 1) {
- // Newline in the middle — structure changed
- textAreaDirtyCache(w);
- return;
- }
- }
- }
-
- // Recompute visual length of the affected line
- int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 4;
- int32_t vc = 0;
-
- for (int32_t i = lineOff; i < nextOff && i < ta->len && buf[i] != '\n'; i++) {
- if (buf[i] == '\t') {
- vc += tabW - (vc % tabW);
- } else {
- vc++;
- }
- }
-
- ta->lineVisLens[line] = vc;
- ta->cachedMaxLL = -1; // recompute lazily
- ta->cachedGutterW = 0;
-}
-
-
-// Incrementally update the cache after inserting bytes at `off`.
-// If the insertion contains newlines, falls back to a full rebuild.
-// Otherwise, adjusts offsets and visual lengths in O(lines_after_cursor).
-static void textAreaCacheInsert(WidgetT *w, int32_t off, int32_t insertLen) {
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
-
- if (ta->cachedLines < 0 || insertLen <= 0) {
- textAreaDirtyCache(w);
- return;
- }
-
- // Check if inserted bytes contain newlines — if so, full rebuild
- const char *buf = ta->buf;
- for (int32_t i = off; i < off + insertLen && i < ta->len; i++) {
- if (buf[i] == '\n') {
- textAreaDirtyCache(w);
- return;
- }
- }
-
- // Find which line the insertion is on
- int32_t line = 0;
- for (line = ta->cachedLines - 1; line > 0; line--) {
- if (ta->lineOffsets[line] <= off) {
- break;
- }
- }
-
- // Shift all subsequent line offsets
- for (int32_t i = line + 1; i <= ta->cachedLines; i++) {
- ta->lineOffsets[i] += insertLen;
- }
-
- // Recompute visual length of the affected line
- int32_t lineOff = ta->lineOffsets[line];
- int32_t lineEnd = (line + 1 <= ta->cachedLines) ? ta->lineOffsets[line + 1] : ta->len;
- int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 4;
- int32_t vc = 0;
-
- for (int32_t i = lineOff; i < lineEnd && buf[i] != '\n'; i++) {
- if (buf[i] == '\t') {
- vc += tabW - (vc % tabW);
- } else {
- vc++;
- }
- }
-
- ta->lineVisLens[line] = vc;
-
- // Update max line length
- if (vc > ta->cachedMaxLL) {
- ta->cachedMaxLL = vc;
- } else {
- ta->cachedMaxLL = -1; // unknown, will recompute lazily
- }
-
- ta->cachedGutterW = 0;
-}
-
-
-static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col) {
- int32_t start = textAreaLineStart(buf, len, row);
- int32_t lineL = textAreaLineLen(buf, len, row);
- int32_t clampC = col < lineL ? col : lineL;
-
- return start + clampC;
-}
-
-
static inline void textAreaDirtyCache(WidgetT *w) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- ta->cachedLines = -1;
- ta->cachedMaxLL = -1;
- ta->cachedGutterW = 0;
-}
-
-
-static void textAreaEnsureCache(WidgetT *w) {
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
-
- if (ta->cachedLines < 0) {
- textAreaRebuildCache(w);
- }
+ textEditLineCacheDirty(&ta->te.lines);
+ ta->cachedGutterW = 0;
}
static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- int32_t row = ta->cursorRow;
-
- // Use cached cursor offset to avoid O(N) recomputation
- int32_t curLineOff = textAreaLineStartCached(w, row);
- int32_t curOff = curLineOff + ta->cursorCol;
-
- if (curOff > ta->len) {
- curOff = ta->len;
- }
-
- int32_t col = visualCol(ta->buf, curLineOff, curOff, ta->tabWidth);
-
- if (row < ta->scrollRow) {
- ta->scrollRow = row;
- }
-
- if (row >= ta->scrollRow + visRows) {
- ta->scrollRow = row - visRows + 1;
- }
-
- if (col < ta->scrollCol) {
- ta->scrollCol = col;
- }
-
- if (col >= ta->scrollCol + visCols) {
- ta->scrollCol = col - visCols + 1;
- }
+ textEditEnsureVisible(&ta->te.lines, ta->te.buf, ta->te.len, ta->te.cursorRow, ta->te.cursorCol, ta->tabWidth, visRows, visCols, &ta->te.scrollRow, &ta->te.scrollCol);
}
static int32_t textAreaGetLineCount(WidgetT *w) {
- textAreaEnsureCache(w);
- return ((TextAreaDataT *)w->data)->cachedLines;
+ TextAreaDataT *ta = (TextAreaDataT *)w->data;
+ return textEditLineCount(&ta->te.lines, ta->te.buf, ta->te.len);
}
static int32_t textAreaGetMaxLineLen(WidgetT *w) {
- textAreaEnsureCache(w);
TextAreaDataT *ta = (TextAreaDataT *)w->data;
-
- if (ta->cachedMaxLL < 0 && ta->cachedLines >= 0) {
- // Recompute max from cached visual lengths
- int32_t maxVL = 0;
-
- for (int32_t i = 0; i < ta->cachedLines; i++) {
- if (ta->lineVisLens[i] > maxVL) {
- maxVL = ta->lineVisLens[i];
- }
- }
-
- ta->cachedMaxLL = maxVL;
- }
-
- return ta->cachedMaxLL;
+ return textEditMaxLineLen(&ta->te.lines, ta->te.buf, ta->te.len);
}
@@ -1161,7 +872,6 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
int32_t totalLines = textAreaGetLineCount(w);
- // Return cached value if line count hasn't changed
if (ta->cachedGutterW > 0 && ta->cachedGutterLines == totalLines) {
return ta->cachedGutterW;
}
@@ -1185,238 +895,6 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
}
-static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) {
- int32_t start = textAreaLineStart(buf, len, row);
- int32_t end = start;
-
- while (end < len && buf[end] != '\n') {
- end++;
- }
-
- return end - start;
-}
-
-
-// O(1) line length via cache
-static int32_t textAreaLineLenCached(WidgetT *w, int32_t row) {
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
- textAreaEnsureCache(w);
-
- if (row < 0 || row >= ta->cachedLines) {
- return 0;
- }
-
- int32_t start = ta->lineOffsets[row];
- int32_t next = ta->lineOffsets[row + 1];
-
- // Subtract newline if present
- if (next > start && ta->buf[next - 1] == '\n') {
- next--;
- }
-
- return next - start;
-}
-
-
-static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) {
- (void)buf;
- (void)len;
- (void)row;
- // Should not be called — use textAreaLineStartCached() instead.
- // Fallback: linear scan (only reachable if cache isn't built yet)
- int32_t off = 0;
-
- for (int32_t r = 0; r < row; r++) {
- while (off < len && buf[off] != '\n') {
- off++;
- }
-
- if (off < len) {
- off++;
- }
- }
-
- return off;
-}
-
-
-// O(1) line start via cache
-static int32_t textAreaLineStartCached(WidgetT *w, int32_t row) {
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
- textAreaEnsureCache(w);
-
- if (row < 0) {
- return 0;
- }
-
- if (row >= ta->cachedLines) {
- return ta->len;
- }
-
- return ta->lineOffsets[row];
-}
-
-
-static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col) {
- int32_t r = 0;
- int32_t c = 0;
-
- for (int32_t i = 0; i < off; i++) {
- if (buf[i] == '\n') {
- r++;
- c = 0;
- } else {
- c++;
- }
- }
-
- *row = r;
- *col = c;
-}
-
-
-// O(log N) offset-to-row/col using binary search on cached line offsets.
-// Falls back to linear scan if cache is not available.
-static void textAreaOffToRowColFast(TextAreaDataT *ta, int32_t off, int32_t *row, int32_t *col) {
- if (!ta->lineOffsets || ta->cachedLines < 0) {
- textAreaOffToRowCol(ta->buf, off, row, col);
- return;
- }
-
- // Binary search for the row containing 'off'
- int32_t lo = 0;
- int32_t hi = ta->cachedLines;
-
- while (lo < hi) {
- int32_t mid = (lo + hi + 1) / 2;
-
- if (ta->lineOffsets[mid] <= off) {
- lo = mid;
- } else {
- hi = mid - 1;
- }
- }
-
- *row = lo;
- *col = off - ta->lineOffsets[lo];
-}
-
-
-// lineOffsets[i] = byte offset where line i starts.
-// lineOffsets[lineCount] = len (sentinel past last line).
-// Built lazily on first access after invalidation (cachedLines == -1).
-// Single O(N) scan builds both the line offset table and per-line
-// visual length cache simultaneously.
-static void textAreaRebuildCache(WidgetT *w) {
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
- char *buf = ta->buf;
- int32_t len = ta->len;
- int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3;
-
- // Count lines first
- int32_t lineCount = 1;
-
- for (int32_t i = 0; i < len; i++) {
- if (buf[i] == '\n') {
- lineCount++;
- }
- }
-
- // Grow line offset array if needed (+1 for sentinel)
- int32_t needed = lineCount + 1;
-
- if (needed > ta->lineOffsetCap) {
- int32_t newCap = needed + TEXTAREA_LINE_CAP_GROWTH;
- ta->lineOffsets = (int32_t *)realloc(ta->lineOffsets, newCap * sizeof(int32_t));
- ta->lineOffsetCap = newCap;
- }
-
- // Grow visual length array if needed
- if (lineCount > ta->lineVisLenCap) {
- int32_t newCap = lineCount + TEXTAREA_LINE_CAP_GROWTH;
- ta->lineVisLens = (int32_t *)realloc(ta->lineVisLens, newCap * sizeof(int32_t));
- ta->lineVisLenCap = newCap;
- }
-
- // Single pass: record offsets and compute visual lengths
- int32_t line = 0;
- int32_t vc = 0;
- int32_t maxVL = 0;
-
- ta->lineOffsets[0] = 0;
-
- for (int32_t i = 0; i < len; i++) {
- if (buf[i] == '\n') {
- ta->lineVisLens[line] = vc;
-
- if (vc > maxVL) {
- maxVL = vc;
- }
-
- line++;
- ta->lineOffsets[line] = i + 1;
- vc = 0;
- } else if (buf[i] == '\t') {
- vc += tabW - (vc % tabW);
- } else {
- vc++;
- }
- }
-
- // Last line (may not end with newline)
- ta->lineVisLens[line] = vc;
-
- if (vc > maxVL) {
- maxVL = vc;
- }
-
- ta->lineOffsets[lineCount] = len;
- ta->cachedLines = lineCount;
- ta->cachedMaxLL = maxVL;
-}
-
-
-// Tab-aware visual column from buffer offset within a line.
-// tabW <= 0 means tabs are 1 column (no expansion).
-static int32_t visualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW) {
- int32_t vc = 0;
-
- for (int32_t i = lineStart; i < off; i++) {
- if (buf[i] == '\t' && tabW > 0) {
- vc += tabW - (vc % tabW);
- } else {
- vc++;
- }
- }
-
- return vc;
-}
-
-
-// Tab-aware: convert visual column to buffer offset within a line.
-static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW) {
- int32_t vc = 0;
- int32_t i = lineStart;
-
- while (i < len && buf[i] != '\n') {
- int32_t w = 1;
-
- if (buf[i] == '\t' && tabW > 0) {
- w = tabW - (vc % tabW);
- }
-
- if (vc + w > targetVC) {
- break;
- }
-
- vc += w;
- i++;
- }
-
- return i;
-}
-
-
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask) {
if (!mask) {
return NULL;
@@ -1484,28 +962,10 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
}
w->data = ta;
- int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
- ta->buf = (char *)malloc(bufSize);
- ta->undoBuf = (char *)malloc(bufSize);
- ta->bufSize = bufSize;
-
- if (!ta->buf || !ta->undoBuf) {
- free(ta->buf);
- free(ta->undoBuf);
- ta->buf = NULL;
- ta->undoBuf = NULL;
- } else {
- ta->buf[0] = '\0';
- }
-
- ta->selAnchor = -1;
- ta->selCursor = -1;
- ta->desiredCol = 0;
- ta->cachedLines = -1;
- ta->cachedMaxLL = -1;
ta->captureTabs = false; // default: Tab moves focus
ta->useTabChar = true; // default: insert actual tab character
ta->tabWidth = 3; // default: 3-space tab stops
+ textEditMultiInit(&ta->te, maxLen, ta->tabWidth);
// Pre-allocate paint buffers (avoids 3KB stack alloc per visible line per frame)
ta->rawSyntax = (uint8_t *)malloc(MAX_COLORIZE_LEN);
@@ -1526,12 +986,12 @@ void wgtTextAreaAppendText(WidgetT *w, const char *text) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- if (!ta->buf) {
+ if (!ta->te.buf) {
return;
}
int32_t addLen = (int32_t)strlen(text);
- int32_t room = ta->bufSize - 1 - ta->len;
+ int32_t room = ta->te.bufSize - 1 - ta->te.len;
if (addLen > room) {
addLen = room;
@@ -1541,96 +1001,48 @@ void wgtTextAreaAppendText(WidgetT *w, const char *text) {
return;
}
- memcpy(ta->buf + ta->len, text, addLen);
- ta->len += addLen;
- ta->buf[ta->len] = '\0';
- ta->cachedLines = -1;
- ta->cachedMaxLL = -1;
+ memcpy(ta->te.buf + ta->te.len, text, addLen);
+ ta->te.len += addLen;
+ ta->te.buf[ta->te.len] = '\0';
+ textAreaDirtyCache(w);
wgtInvalidate(w);
}
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) {
- if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) {
+ if (!w || w->type != sTextAreaTypeId) {
return false;
}
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- int32_t needleLen = strlen(needle);
+ bool found = textEditFindNext(&ta->te.lines, ta->te.buf, ta->te.len, needle, caseSensitive, forward, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selAnchor, &ta->te.selCursor);
- if (needleLen > ta->len) {
- return false;
- }
+ if (found) {
+ AppContextT *ctx = wgtGetContext(w);
- // Get current cursor byte position
- int32_t cursorByte = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
+ if (ctx) {
+ const BitmapFontT *font = &ctx->font;
+ int32_t gutterW = textAreaGutterWidth(w, font);
+ int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
+ int32_t visCols = innerW / font->charWidth;
+ int32_t innerH = w->h - TEXTAREA_BORDER * 2;
+ int32_t visRows = innerH / font->charHeight;
- // Search forward from cursor+1 (or backward from cursor-1).
- // No wrap-around: returns false if the end/start of the buffer
- // is reached without finding a match.
- int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
- int32_t searchLen = ta->len - needleLen + 1;
-
- if (searchLen <= 0) {
- return false;
- }
-
- int32_t count = forward ? (searchLen - startPos) : (startPos + 1);
-
- if (count <= 0) {
- return false;
- }
-
- for (int32_t attempt = 0; attempt < count; attempt++) {
- int32_t pos;
-
- if (forward) {
- pos = startPos + attempt;
- } else {
- pos = startPos - attempt;
- }
-
- if (pos < 0 || pos >= searchLen) {
- break;
- }
-
- bool match;
- if (caseSensitive) {
- match = (memcmp(ta->buf + pos, needle, needleLen) == 0);
- } else {
- match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0);
- }
-
- if (match) {
- // Select the found text
- ta->selAnchor = pos;
- ta->selCursor = pos + needleLen;
-
- // Move cursor to start of match (forward) or end of match (backward)
- int32_t cursorOff = forward ? pos : pos + needleLen;
- textAreaOffToRowColFast(ta, cursorOff, &ta->cursorRow, &ta->cursorCol);
- ta->desiredCol = ta->cursorCol;
-
- // Scroll to show the match
- AppContextT *ctx = wgtGetContext(w);
- if (ctx) {
- const BitmapFontT *font = &ctx->font;
- int32_t gutterW = textAreaGutterWidth(w, font);
- int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
- int32_t visCols = innerW / font->charWidth;
- int32_t innerH = w->h - TEXTAREA_BORDER * 2;
- int32_t visRows = innerH / font->charHeight;
- if (visCols < 1) { visCols = 1; }
- if (visRows < 1) { visRows = 1; }
- textAreaEnsureVisible(w, visRows, visCols);
+ if (visCols < 1) {
+ visCols = 1;
}
- wgtInvalidatePaint(w);
- return true;
+ if (visRows < 1) {
+ visRows = 1;
+ }
+
+ textAreaEnsureVisible(w, visRows, visCols);
}
+
+ wgtInvalidatePaint(w);
}
- return false;
+ return found;
}
@@ -1640,38 +1052,18 @@ int32_t wgtTextAreaGetCursorLine(const WidgetT *w) {
}
const TextAreaDataT *ta = (const TextAreaDataT *)w->data;
- return ta->cursorRow + 1; // 0-based to 1-based
+ return ta->te.cursorRow + 1; // 0-based to 1-based
}
int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize) {
- buf[0] = '\0';
-
- if (!w || w->type != sTextAreaTypeId || bufSize < 2) {
+ if (!w || w->type != sTextAreaTypeId) {
+ buf[0] = '\0';
return 0;
}
- const TextAreaDataT *ta = (const TextAreaDataT *)w->data;
- if (!ta || !ta->buf || ta->len == 0) {
- return 0;
- }
-
- int32_t off = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
- int32_t ws = wordStart(ta->buf, off);
- int32_t we = wordEnd(ta->buf, ta->len, off);
- int32_t len = we - ws;
-
- if (len <= 0) {
- return 0;
- }
-
- if (len >= bufSize) {
- len = bufSize - 1;
- }
-
- memcpy(buf, ta->buf + ws, len);
- buf[len] = '\0';
- return len;
+ TextAreaDataT *ta = (TextAreaDataT *)w->data;
+ return textEditGetWordAtCursor(&ta->te.lines, ta->te.buf, ta->te.len, ta->te.cursorRow, ta->te.cursorCol, buf, bufSize);
}
@@ -1680,129 +1072,35 @@ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) {
return;
}
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
- int32_t totalLines = textAreaGetLineCount(w);
- int32_t row = line - 1; // 1-based to 0-based
-
- if (row < 0) {
- row = 0;
- }
-
- if (row >= totalLines) {
- row = totalLines - 1;
- }
-
- ta->cursorRow = row;
- ta->cursorCol = 0;
- ta->desiredCol = 0;
-
- // Select the entire line for visual highlight
- int32_t lineStart = textAreaLineStartCached(w, row);
- int32_t lineL = textAreaLineLenCached(w, row);
- ta->selAnchor = lineStart;
- ta->selCursor = lineStart + lineL;
-
- // Scroll to put the target line near the top of the visible area
- AppContextT *ctx = wgtGetContext(w);
+ TextAreaDataT *ta = (TextAreaDataT *)w->data;
+ AppContextT *ctx = wgtGetContext(w);
+ int32_t visRows = 1;
if (ctx) {
const BitmapFontT *font = &ctx->font;
- int32_t gutterW = textAreaGutterWidth(w, font);
- int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
- int32_t visCols = innerW / font->charWidth;
- int32_t innerH = w->h - TEXTAREA_BORDER * 2;
- int32_t visRows = innerH / font->charHeight;
+ int32_t innerH = w->h - TEXTAREA_BORDER * 2;
+ visRows = innerH / font->charHeight;
- if (visCols < 1) { visCols = 1; }
- if (visRows < 1) { visRows = 1; }
-
- // Place target line 1/4 from top for context, then clamp
- int32_t targetScroll = row - visRows / 4;
-
- if (targetScroll < 0) {
- targetScroll = 0;
+ if (visRows < 1) {
+ visRows = 1;
}
-
- int32_t maxScroll = totalLines - visRows;
-
- if (maxScroll < 0) {
- maxScroll = 0;
- }
-
- if (targetScroll > maxScroll) {
- targetScroll = maxScroll;
- }
-
- ta->scrollRow = targetScroll;
- ta->scrollCol = 0;
}
+ textEditGoToLine(&ta->te.lines, ta->te.buf, ta->te.len, line, visRows, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.scrollRow, &ta->te.scrollCol, &ta->te.selAnchor, &ta->te.selCursor);
wgtInvalidatePaint(w);
}
int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive) {
- if (!w || w->type != sTextAreaTypeId || !needle || !needle[0] || !replacement) {
+ if (!w || w->type != sTextAreaTypeId) {
return 0;
}
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- int32_t needleLen = strlen(needle);
- int32_t replLen = strlen(replacement);
- int32_t delta = replLen - needleLen;
- int32_t count = 0;
-
- if (needleLen > ta->len) {
- return 0;
- }
-
- // Save undo before any modifications
- textEditSaveUndo(ta->buf, ta->len, textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol), ta->undoBuf, &ta->undoLen, &ta->undoCursor, ta->bufSize);
-
- int32_t pos = 0;
- while (pos + needleLen <= ta->len) {
- bool match;
- if (caseSensitive) {
- match = (memcmp(ta->buf + pos, needle, needleLen) == 0);
- } else {
- match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0);
- }
-
- if (match) {
- // Check if replacement fits in buffer
- if (ta->len + delta >= ta->bufSize) {
- break;
- }
-
- // Shift text after match to make room (or close gap)
- if (delta != 0) {
- memmove(ta->buf + pos + replLen, ta->buf + pos + needleLen, ta->len - pos - needleLen);
- }
-
- // Copy replacement in
- memcpy(ta->buf + pos, replacement, replLen);
- ta->len += delta;
- ta->buf[ta->len] = '\0';
- count++;
- pos += replLen;
- } else {
- pos++;
- }
- }
+ int32_t count = textEditReplaceAll(&ta->te.lines, ta->te.buf, ta->te.bufSize, &ta->te.len, needle, replacement, caseSensitive, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selAnchor, &ta->te.selCursor, ta->te.undoBuf, &ta->te.undoLen, &ta->te.undoCursor);
if (count > 0) {
- // Clear selection
- ta->selAnchor = -1;
- ta->selCursor = -1;
-
- // Clamp cursor if it's past the end
- int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
- if (cursorOff > ta->len) {
- textAreaOffToRowColFast(ta, ta->len, &ta->cursorRow, &ta->cursorCol);
- }
-
- ta->desiredCol = ta->cursorCol;
- textAreaDirtyCache(w);
+ ta->cachedGutterW = 0;
wgtInvalidatePaint(w);
if (w->onChange) {
@@ -1887,8 +1185,8 @@ void wgtTextAreaSetSyntaxColors(WidgetT *w, const uint32_t *colors, int32_t coun
memset(ta->customSyntaxColors, 0, sizeof(ta->customSyntaxColors));
if (colors && count > 0) {
- if (count > SYNTAX_MAX) {
- count = SYNTAX_MAX;
+ if (count > TEXT_SYNTAX_MAX) {
+ count = TEXT_SYNTAX_MAX;
}
memcpy(ta->customSyntaxColors, colors, count * sizeof(uint32_t));
@@ -1905,6 +1203,7 @@ void wgtTextAreaSetTabWidth(WidgetT *w, int32_t width) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
ta->tabWidth = width > 0 ? width : 1;
+ textEditLineCacheSetTabWidth(&ta->te.lines, ta->tabWidth);
}
@@ -1956,9 +1255,9 @@ void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font) {
static bool widgetTextAreaClearSelection(WidgetT *w) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- if (ta->selAnchor >= 0 && ta->selCursor >= 0 && ta->selAnchor != ta->selCursor) {
- ta->selAnchor = -1;
- ta->selCursor = -1;
+ if (ta->te.selAnchor >= 0 && ta->te.selCursor >= 0 && ta->te.selAnchor != ta->te.selCursor) {
+ ta->te.selAnchor = -1;
+ ta->te.selCursor = -1;
return true;
}
@@ -1970,10 +1269,7 @@ void widgetTextAreaDestroy(WidgetT *w) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
if (ta) {
- free(ta->buf);
- free(ta->undoBuf);
- free(ta->lineOffsets);
- free(ta->lineVisLens);
+ textEditMultiFree(&ta->te);
free(ta->rawSyntax);
free(ta->expandBuf);
free(ta->syntaxBuf);
@@ -1985,60 +1281,21 @@ void widgetTextAreaDestroy(WidgetT *w) {
static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
- TextAreaDataT *ta = (TextAreaDataT *)w->data;
- AppContextT *ctx = wgtGetContext(w);
+ TextAreaDataT *ta = (TextAreaDataT *)w->data;
+ AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font;
+ int32_t gutterW = textAreaGutterWidth(w, font);
+ int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW;
+ int32_t textY = w->y + TEXTAREA_BORDER;
+ int32_t visRows = (w->h - TEXTAREA_BORDER * 2) / font->charHeight;
- int32_t gutterW = textAreaGutterWidth(w, font);
- int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW;
- int32_t innerY = w->y + TEXTAREA_BORDER;
- int32_t relX = vx - innerX;
- int32_t relY = vy - innerY;
-
- int32_t totalLines = textAreaGetLineCount(w);
- int32_t visRows = (w->h - TEXTAREA_BORDER * 2) / font->charHeight;
-
- // Auto-scroll when dragging past edges
- if (relY < 0 && ta->scrollRow > 0) {
- ta->scrollRow--;
- } else if (relY >= visRows * font->charHeight && ta->scrollRow < totalLines - visRows) {
- ta->scrollRow++;
- }
-
- int32_t clickRow = ta->scrollRow + relY / font->charHeight;
- int32_t clickVisCol = ta->scrollCol + relX / font->charWidth;
-
- if (clickRow < 0) {
- clickRow = 0;
- }
-
- if (clickRow >= totalLines) {
- clickRow = totalLines - 1;
- }
-
- if (clickVisCol < 0) {
- clickVisCol = 0;
- }
-
- int32_t dragLineStart = textAreaLineStartCached(w, clickRow);
- int32_t dragByteOff = visualColToOff(ta->buf, ta->len, dragLineStart, clickVisCol, ta->tabWidth);
- int32_t clickCol = dragByteOff - dragLineStart;
- int32_t lineL = textAreaLineLenCached(w, clickRow);
-
- if (clickCol > lineL) {
- clickCol = lineL;
- }
-
- ta->cursorRow = clickRow;
- ta->cursorCol = clickCol;
- ta->desiredCol = clickCol;
- ta->selCursor = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol);
+ widgetTextEditMultiDragUpdateArea(&ta->te.lines, ta->te.buf, ta->te.len, vx, vy, textX, textY, font, &ta->te.scrollRow, ta->te.scrollCol, visRows, ta->tabWidth, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selCursor);
}
const char *widgetTextAreaGetText(const WidgetT *w) {
const TextAreaDataT *ta = (const TextAreaDataT *)w->data;
- return ta->buf ? ta->buf : "";
+ return ta->te.buf ? ta->te.buf : "";
}
@@ -2073,30 +1330,21 @@ static void widgetTextAreaOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int
void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- if (!ta->buf) {
+ if (!ta->te.buf) {
return;
}
clearOtherSelections(w);
- char *buf = ta->buf;
- int32_t bufSize = ta->bufSize;
- int32_t *pLen = &ta->len;
- int32_t *pRow = &ta->cursorRow;
- int32_t *pCol = &ta->cursorCol;
- int32_t *pSA = &ta->selAnchor;
- int32_t *pSC = &ta->selCursor;
- bool shift = (mod & KEY_MOD_SHIFT) != 0;
-
- AppContextT *ctx = wgtGetContext(w);
+ AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font;
- int32_t gutterW = textAreaGutterWidth(w, font);
- int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
- int32_t visCols = innerW / font->charWidth;
- int32_t maxLL = textAreaGetMaxLineLen(w);
- bool needHSb = (maxLL > visCols);
- int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
- int32_t visRows = innerH / font->charHeight;
+ int32_t gutterW = textAreaGutterWidth(w, font);
+ int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
+ int32_t visCols = innerW / font->charWidth;
+ int32_t maxLL = textAreaGetMaxLineLen(w);
+ bool needHSb = (maxLL > visCols);
+ int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
+ int32_t visRows = innerH / font->charHeight;
if (visRows < 1) {
visRows = 1;
@@ -2106,579 +1354,18 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
visCols = 1;
}
- int32_t totalLines = textAreaGetLineCount(w);
-
- // Helper macros for cursor offset
- #define CUR_OFF() textAreaCursorToOff(buf, *pLen, *pRow, *pCol)
-
- // Start/extend selection
- #define SEL_BEGIN() do { \
- if (shift && *pSA < 0) { *pSA = CUR_OFF(); *pSC = *pSA; } \
- } while (0)
-
- #define SEL_END() do { \
- if (shift) { *pSC = CUR_OFF(); } \
- else { *pSA = -1; *pSC = -1; } \
- } while (0)
-
- #define HAS_SEL() (*pSA >= 0 && *pSC >= 0 && *pSA != *pSC)
-
- #define SEL_LO() (*pSA < *pSC ? *pSA : *pSC)
- #define SEL_HI() (*pSA < *pSC ? *pSC : *pSA)
-
- // Clamp selection to buffer bounds
- if (HAS_SEL()) {
- if (*pSA > *pLen) {
- *pSA = *pLen;
- }
-
- if (*pSC > *pLen) {
- *pSC = *pLen;
- }
- }
-
- // Ctrl+A -- select all
- if (key == 1) {
- *pSA = 0;
- *pSC = *pLen;
- textAreaOffToRowColFast(ta, *pLen, pRow, pCol);
- ta->desiredCol = *pCol;
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+C -- copy
- if (key == KEY_CTRL_C) {
- if (HAS_SEL()) {
- clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
- }
-
- return;
- }
-
- // Read-only: allow select-all, copy, and navigation but block editing
- if (w->readOnly) {
- goto navigation;
- }
-
- // Ctrl+V -- paste
- if (key == KEY_CTRL_V) {
- int32_t clipLen = 0;
- const char *clip = clipboardGet(&clipLen);
-
- if (clipLen > 0) {
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
-
- if (HAS_SEL()) {
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- }
-
- int32_t off = CUR_OFF();
- int32_t canFit = bufSize - 1 - *pLen;
- int32_t paste = clipLen < canFit ? clipLen : canFit;
-
- if (paste > 0) {
- memmove(buf + off + paste, buf + off, *pLen - off + 1);
- memcpy(buf + off, clip, paste);
- *pLen += paste;
- textAreaOffToRowColFast(ta, off + paste, pRow, pCol);
- ta->desiredCol = *pCol;
- }
-
- if (w->onChange) {
- w->onChange(w);
- }
-
- textAreaDirtyCache(w);
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+X -- cut
- if (key == KEY_CTRL_X) {
- if (HAS_SEL()) {
- clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
-
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- ta->desiredCol = *pCol;
-
- if (w->onChange) {
- w->onChange(w);
- }
-
- textAreaDirtyCache(w);
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+Z -- undo
- if (key == KEY_CTRL_Z) {
- if (ta->undoBuf && ta->undoLen >= 0) {
- // Swap current and undo
- char tmpBuf[*pLen + 1];
- int32_t tmpLen = *pLen;
- int32_t tmpCursor = CUR_OFF();
- int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;
-
- memcpy(tmpBuf, buf, copyLen);
- tmpBuf[copyLen] = '\0';
-
- int32_t restLen = ta->undoLen < bufSize - 1 ? ta->undoLen : bufSize - 1;
- memcpy(buf, ta->undoBuf, restLen);
- buf[restLen] = '\0';
- *pLen = restLen;
-
- // Save current as new undo
- int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
- memcpy(ta->undoBuf, tmpBuf, saveLen);
- ta->undoBuf[saveLen] = '\0';
- ta->undoLen = saveLen;
- ta->undoCursor = tmpCursor;
-
- // Restore cursor
- int32_t restoreOff = ta->undoCursor < *pLen ? ta->undoCursor : *pLen;
- ta->undoCursor = tmpCursor;
- textAreaOffToRowColFast(ta, restoreOff, pRow, pCol);
- ta->desiredCol = *pCol;
- *pSA = -1;
- *pSC = -1;
-
- if (w->onChange) {
- w->onChange(w);
- }
-
- textAreaDirtyCache(w);
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Enter -- insert newline
- if (key == 0x0D) {
- if (*pLen < bufSize - 1) {
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
-
- if (HAS_SEL()) {
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- }
-
- int32_t off = CUR_OFF();
-
- // Measure indent of current line before inserting newline
- int32_t indent = 0;
- char indentBuf[64];
-
- if (ta->autoIndent) {
- int32_t lineStart = textAreaLineStart(buf, *pLen, *pRow);
-
- while (lineStart + indent < off && indent < 63 && (buf[lineStart + indent] == ' ' || buf[lineStart + indent] == '\t')) {
- indentBuf[indent] = buf[lineStart + indent];
- indent++;
- }
- }
-
- if (*pLen + 1 + indent < bufSize) {
- memmove(buf + off + 1 + indent, buf + off, *pLen - off + 1);
- buf[off] = '\n';
- memcpy(buf + off + 1, indentBuf, indent);
- *pLen += 1 + indent;
- (*pRow)++;
- *pCol = indent;
- ta->desiredCol = indent;
- }
-
- if (w->onChange) {
- w->onChange(w);
- }
-
- textAreaDirtyCache(w);
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Backspace
- if (key == 8) {
- if (HAS_SEL()) {
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- ta->desiredCol = *pCol;
- textAreaDirtyCache(w);
-
- if (w->onChange) {
- w->onChange(w);
- }
- } else {
- int32_t off = CUR_OFF();
-
- if (off > 0) {
- char deleted = buf[off - 1];
- textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
- memmove(buf + off - 1, buf + off, *pLen - off + 1);
- (*pLen)--;
- textAreaOffToRowColFast(ta, off - 1, pRow, pCol);
- ta->desiredCol = *pCol;
-
- if (deleted == '\n') {
- textAreaDirtyCache(w);
- } else {
- textAreaCacheDelete(w, off - 1, 1);
- }
-
- if (w->onChange) {
- w->onChange(w);
- }
- }
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Delete
- if (key == KEY_DELETE) {
- if (HAS_SEL()) {
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- ta->desiredCol = *pCol;
- textAreaDirtyCache(w);
-
- if (w->onChange) {
- w->onChange(w);
- }
- } else {
- int32_t off = CUR_OFF();
-
- if (off < *pLen) {
- char deleted = buf[off];
- textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
- memmove(buf + off, buf + off + 1, *pLen - off);
- (*pLen)--;
-
- if (deleted == '\n') {
- textAreaDirtyCache(w);
- } else {
- textAreaCacheDelete(w, off, 1);
- }
-
- if (w->onChange) {
- w->onChange(w);
- }
- }
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
-navigation:
- // Left arrow
- if (key == KEY_LEFT) {
- SEL_BEGIN();
- int32_t off = CUR_OFF();
-
- if (off > 0) {
- textAreaOffToRowColFast(ta, off - 1, pRow, pCol);
- }
-
- ta->desiredCol = *pCol;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Right arrow
- if (key == KEY_RIGHT) {
- SEL_BEGIN();
- int32_t off = CUR_OFF();
-
- if (off < *pLen) {
- textAreaOffToRowColFast(ta, off + 1, pRow, pCol);
- }
-
- ta->desiredCol = *pCol;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+Left -- word left
- if (key == (0x73 | 0x100)) {
- SEL_BEGIN();
- int32_t off = CUR_OFF();
- int32_t newOff = wordBoundaryLeft(buf, off);
- textAreaOffToRowColFast(ta, newOff, pRow, pCol);
- ta->desiredCol = *pCol;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+Right -- word right
- if (key == (0x74 | 0x100)) {
- SEL_BEGIN();
- int32_t off = CUR_OFF();
- int32_t newOff = wordBoundaryRight(buf, *pLen, off);
- textAreaOffToRowColFast(ta, newOff, pRow, pCol);
- ta->desiredCol = *pCol;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Up arrow
- if (key == KEY_UP) {
- SEL_BEGIN();
-
- if (*pRow > 0) {
- (*pRow)--;
- int32_t lineL = textAreaLineLen(buf, *pLen, *pRow);
- *pCol = ta->desiredCol < lineL ? ta->desiredCol : lineL;
- }
-
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Down arrow
- if (key == KEY_DOWN) {
- SEL_BEGIN();
-
- if (*pRow < totalLines - 1) {
- (*pRow)++;
- int32_t lineL = textAreaLineLen(buf, *pLen, *pRow);
- *pCol = ta->desiredCol < lineL ? ta->desiredCol : lineL;
- }
-
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Home
- if (key == KEY_HOME) {
- SEL_BEGIN();
- *pCol = 0;
- ta->desiredCol = 0;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // End
- if (key == KEY_END) {
- SEL_BEGIN();
- *pCol = textAreaLineLen(buf, *pLen, *pRow);
- ta->desiredCol = *pCol;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Page Up
- if (key == KEY_PGUP) {
- SEL_BEGIN();
- *pRow -= visRows;
-
- if (*pRow < 0) {
- *pRow = 0;
- }
-
- int32_t lineL = textAreaLineLen(buf, *pLen, *pRow);
- *pCol = ta->desiredCol < lineL ? ta->desiredCol : lineL;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Page Down
- if (key == KEY_PGDN) {
- SEL_BEGIN();
- *pRow += visRows;
-
- if (*pRow >= totalLines) {
- *pRow = totalLines - 1;
- }
-
- int32_t lineL = textAreaLineLen(buf, *pLen, *pRow);
- *pCol = ta->desiredCol < lineL ? ta->desiredCol : lineL;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+Home (scancode 0x77)
- if (key == (0x77 | 0x100)) {
- SEL_BEGIN();
- *pRow = 0;
- *pCol = 0;
- ta->desiredCol = 0;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Ctrl+End (scancode 0x75)
- if (key == (0x75 | 0x100)) {
- SEL_BEGIN();
- textAreaOffToRowColFast(ta, *pLen, pRow, pCol);
- ta->desiredCol = *pCol;
- SEL_END();
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- // Tab key -- insert tab character or spaces if captureTabs is enabled
- if (key == 9 && ta->captureTabs && !w->readOnly) {
- if (*pLen < bufSize - 1) {
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
-
- if (HAS_SEL()) {
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- }
-
- int32_t off = CUR_OFF();
-
- if (ta->useTabChar) {
- // Insert a single tab character
- if (*pLen < bufSize - 1) {
- memmove(buf + off + 1, buf + off, *pLen - off + 1);
- buf[off] = '\t';
- (*pLen)++;
- (*pCol)++;
- ta->desiredCol = *pCol;
- }
- } else {
- // Insert spaces to next tab stop
- int32_t tw = ta->tabWidth > 0 ? ta->tabWidth : 3;
- int32_t spaces = tw - (*pCol % tw);
-
- for (int32_t s = 0; s < spaces && *pLen < bufSize - 1; s++) {
- memmove(buf + off + 1, buf + off, *pLen - off + 1);
- buf[off] = ' ';
- off++;
- (*pLen)++;
- (*pCol)++;
- }
-
- ta->desiredCol = *pCol;
- }
-
- textAreaDirtyCache(w);
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- }
-
- return;
- }
-
- // Printable character (blocked in read-only mode)
- if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && !w->readOnly) {
- if (*pLen < bufSize - 1) {
- textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
-
- if (HAS_SEL()) {
- int32_t lo = SEL_LO();
- int32_t hi = SEL_HI();
- memmove(buf + lo, buf + hi, *pLen - hi + 1);
- *pLen -= (hi - lo);
- textAreaOffToRowColFast(ta, lo, pRow, pCol);
- *pSA = -1;
- *pSC = -1;
- }
-
- int32_t off = CUR_OFF();
-
- if (*pLen < bufSize - 1) {
- memmove(buf + off + 1, buf + off, *pLen - off + 1);
- buf[off] = (char)key;
- (*pLen)++;
- (*pCol)++;
- ta->desiredCol = *pCol;
- textAreaCacheInsert(w, off, 1);
- } else {
- textAreaDirtyCache(w);
- }
-
- if (w->onChange) {
- w->onChange(w);
- }
- }
-
- textAreaEnsureVisible(w, visRows, visCols);
- wgtInvalidatePaint(w);
- return;
- }
-
- #undef CUR_OFF
- #undef SEL_BEGIN
- #undef SEL_END
- #undef HAS_SEL
- #undef SEL_LO
- #undef SEL_HI
+ TextEditMultiOptionsT opts = {
+ .autoIndent = ta->autoIndent,
+ .captureTabs = ta->captureTabs,
+ .useTabChar = ta->useTabChar,
+ .tabWidth = ta->tabWidth,
+ .readOnly = w->readOnly,
+ };
+
+ widgetTextEditMultiOnKey(w, key, mod, &ta->te.lines, ta->te.buf, ta->te.bufSize, &ta->te.len, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.scrollRow, &ta->te.scrollCol, &ta->te.selAnchor, &ta->te.selCursor, ta->te.undoBuf, &ta->te.undoLen, &ta->te.undoCursor, visRows, visCols, &opts);
+
+ // Gutter cache may be stale after edits that change line count
+ ta->cachedGutterW = 0;
}
@@ -2723,7 +1410,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
maxScroll = 0;
}
- ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll);
+ ta->te.scrollRow = clampInt(ta->te.scrollRow, 0, maxScroll);
int32_t maxHScroll = maxLL - visCols;
@@ -2731,93 +1418,76 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
maxHScroll = 0;
}
- ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll);
+ ta->te.scrollCol = clampInt(ta->te.scrollCol, 0, maxHScroll);
- // Check horizontal scrollbar click
if (needHSb) {
+ int32_t hsbX = w->x + TEXTAREA_BORDER;
int32_t hsbY = w->y + w->h - TEXTAREA_BORDER - TEXTAREA_SB_W;
+ int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
- if (vy >= hsbY && vx < w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W) {
- int32_t hsbX = w->x + TEXTAREA_BORDER;
- int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
- int32_t relX = vx - hsbX;
- int32_t trackLen = hsbW - TEXTAREA_SB_W * 2;
+ if (vy >= hsbY && vx < hsbX + hsbW) {
+ int32_t dragOff = 0;
+ int32_t hit = widgetTextScrollbarHitTest(false, vx, vy, hsbX, hsbY, TEXTAREA_SB_W, hsbW, maxLL, visCols, ta->te.scrollCol, &dragOff);
- if (relX < TEXTAREA_SB_W) {
- // Left arrow
- if (ta->scrollCol > 0) {
- ta->scrollCol--;
- }
- } else if (relX >= hsbW - TEXTAREA_SB_W) {
- // Right arrow
- if (ta->scrollCol < maxHScroll) {
- ta->scrollCol++;
- }
- } else if (trackLen > 0) {
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, maxLL, visCols, ta->scrollCol, &thumbPos, &thumbSize);
-
- int32_t trackRelX = relX - TEXTAREA_SB_W;
-
- if (trackRelX < thumbPos) {
- ta->scrollCol -= visCols;
- ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll);
- } else if (trackRelX >= thumbPos + thumbSize) {
- ta->scrollCol += visCols;
- ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll);
- } else {
- sDragWidget = w;
+ switch (hit) {
+ case TEXT_SB_HIT_UP:
+ if (ta->te.scrollCol > 0) {
+ ta->te.scrollCol--;
+ }
+ break;
+ case TEXT_SB_HIT_DOWN:
+ if (ta->te.scrollCol < maxHScroll) {
+ ta->te.scrollCol++;
+ }
+ break;
+ case TEXT_SB_HIT_PAGE_UP:
+ ta->te.scrollCol = clampInt(ta->te.scrollCol - visCols, 0, maxHScroll);
+ break;
+ case TEXT_SB_HIT_PAGE_DOWN:
+ ta->te.scrollCol = clampInt(ta->te.scrollCol + visCols, 0, maxHScroll);
+ break;
+ case TEXT_SB_HIT_THUMB:
+ sDragWidget = w;
ta->sbDragOrient = 1;
- ta->sbDragging = true;
- ta->sbDragOff = trackRelX - thumbPos;
+ ta->sbDragging = true;
+ ta->sbDragOff = dragOff;
return;
- }
}
return;
}
}
- // Check vertical scrollbar click
int32_t sbX = w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W;
+ int32_t sbY = w->y + TEXTAREA_BORDER;
if (vx >= sbX) {
- int32_t sbY = w->y + TEXTAREA_BORDER;
- int32_t sbH = innerH;
- int32_t relY = vy - sbY;
- int32_t trackLen = sbH - TEXTAREA_SB_W * 2;
+ int32_t dragOff = 0;
+ int32_t hit = widgetTextScrollbarHitTest(true, vx, vy, sbX, sbY, TEXTAREA_SB_W, innerH, totalLines, visRows, ta->te.scrollRow, &dragOff);
- if (relY < TEXTAREA_SB_W) {
- // Up arrow
- if (ta->scrollRow > 0) {
- ta->scrollRow--;
- }
- } else if (relY >= sbH - TEXTAREA_SB_W) {
- // Down arrow
- if (ta->scrollRow < maxScroll) {
- ta->scrollRow++;
- }
- } else if (trackLen > 0) {
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, totalLines, visRows, ta->scrollRow, &thumbPos, &thumbSize);
-
- int32_t trackRelY = relY - TEXTAREA_SB_W;
-
- if (trackRelY < thumbPos) {
- ta->scrollRow -= visRows;
- ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll);
- } else if (trackRelY >= thumbPos + thumbSize) {
- ta->scrollRow += visRows;
- ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll);
- } else {
- sDragWidget = w;
+ switch (hit) {
+ case TEXT_SB_HIT_UP:
+ if (ta->te.scrollRow > 0) {
+ ta->te.scrollRow--;
+ }
+ break;
+ case TEXT_SB_HIT_DOWN:
+ if (ta->te.scrollRow < maxScroll) {
+ ta->te.scrollRow++;
+ }
+ break;
+ case TEXT_SB_HIT_PAGE_UP:
+ ta->te.scrollRow = clampInt(ta->te.scrollRow - visRows, 0, maxScroll);
+ break;
+ case TEXT_SB_HIT_PAGE_DOWN:
+ ta->te.scrollRow = clampInt(ta->te.scrollRow + visRows, 0, maxScroll);
+ break;
+ case TEXT_SB_HIT_THUMB:
+ sDragWidget = w;
ta->sbDragOrient = 0;
- ta->sbDragging = true;
- ta->sbDragOff = trackRelY - thumbPos;
+ ta->sbDragging = true;
+ ta->sbDragOff = dragOff;
return;
- }
}
return;
@@ -2826,7 +1496,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// Click on gutter — toggle breakpoint
if (gutterW > 0 && vx < innerX && ta->onGutterClick) {
int32_t relY = vy - innerY;
- int32_t clickRow = ta->scrollRow + relY / font->charHeight;
+ int32_t clickRow = ta->te.scrollRow + relY / font->charHeight;
if (clickRow >= 0 && clickRow < totalLines) {
ta->onGutterClick(w, clickRow + 1); // 1-based line number
@@ -2836,78 +1506,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
return;
}
- // Click on text area -- place cursor
- int32_t relX = vx - innerX;
- int32_t relY = vy - innerY;
-
- int32_t clickRow = ta->scrollRow + relY / font->charHeight;
- int32_t clickVisCol = ta->scrollCol + relX / font->charWidth;
-
- if (clickRow < 0) {
- clickRow = 0;
- }
-
- if (clickRow >= totalLines) {
- clickRow = totalLines - 1;
- }
-
- if (clickVisCol < 0) {
- clickVisCol = 0;
- }
-
- int32_t clkLineStart = textAreaLineStartCached(w, clickRow);
- int32_t clkByteOff = visualColToOff(ta->buf, ta->len, clkLineStart, clickVisCol, ta->tabWidth);
- int32_t clickCol = clkByteOff - clkLineStart;
- int32_t lineL = textAreaLineLenCached(w, clickRow);
-
- if (clickCol > lineL) {
- clickCol = lineL;
- }
-
- int32_t clicks = multiClickDetect(vx, vy);
-
- if (clicks >= 3) {
- // Triple-click: select entire line
- int32_t lineStart = textAreaCursorToOff(ta->buf, ta->len, clickRow, 0);
- int32_t lineEnd = textAreaCursorToOff(ta->buf, ta->len, clickRow, lineL);
-
- ta->cursorRow = clickRow;
- ta->cursorCol = lineL;
- ta->desiredCol = lineL;
- ta->selAnchor = lineStart;
- ta->selCursor = lineEnd;
- sDragWidget = NULL;
- return;
- }
-
- if (clicks == 2 && ta->buf) {
- // Double-click: select word
- int32_t off = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol);
- int32_t ws = wordStart(ta->buf, off);
- int32_t we = wordEnd(ta->buf, ta->len, off);
-
- int32_t weRow;
- int32_t weCol;
- textAreaOffToRowColFast(ta, we, &weRow, &weCol);
-
- ta->cursorRow = weRow;
- ta->cursorCol = weCol;
- ta->desiredCol = weCol;
- ta->selAnchor = ws;
- ta->selCursor = we;
- sDragWidget = NULL;
- return;
- }
-
- // Single click: place cursor + start drag-select
- ta->cursorRow = clickRow;
- ta->cursorCol = clickCol;
- ta->desiredCol = clickCol;
-
- int32_t anchorOff = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol);
- ta->selAnchor = anchorOff;
- ta->selCursor = anchorOff;
- sDragWidget = w;
+ widgetTextEditMultiMouseClick(w, &ta->te.lines, ta->te.buf, ta->te.len, vx, vy, innerX, innerY, font, ta->te.scrollRow, ta->te.scrollCol, ta->tabWidth, &ta->te.cursorRow, &ta->te.cursorCol, &ta->te.desiredCol, &ta->te.selAnchor, &ta->te.selCursor);
ta->sbDragging = false;
}
@@ -2934,8 +1533,6 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
- char *buf = ta->buf;
- int32_t len = ta->len;
int32_t gutterW = textAreaGutterWidth(w, font);
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW;
int32_t visCols = innerW / font->charWidth;
@@ -2950,86 +1547,54 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2);
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
- // Clamp vertical scroll
int32_t maxScroll = totalLines - visRows;
if (maxScroll < 0) {
maxScroll = 0;
}
- ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll);
+ ta->te.scrollRow = clampInt(ta->te.scrollRow, 0, maxScroll);
- // Clamp horizontal scroll
int32_t maxHScroll = maxLL - visCols;
if (maxHScroll < 0) {
maxHScroll = 0;
}
- ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll);
+ ta->te.scrollCol = clampInt(ta->te.scrollCol, 0, maxHScroll);
- // Selection range
- int32_t selLo = -1;
- int32_t selHi = -1;
-
- if (ta->selAnchor >= 0 && ta->selCursor >= 0 && ta->selAnchor != ta->selCursor) {
- selLo = ta->selAnchor < ta->selCursor ? ta->selAnchor : ta->selCursor;
- selHi = ta->selAnchor < ta->selCursor ? ta->selCursor : ta->selAnchor;
- }
-
- // Draw lines -- compute first visible line offset once, then advance incrementally
int32_t gutterX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD;
int32_t textX = gutterX + gutterW;
int32_t textY = w->y + TEXTAREA_BORDER;
- int32_t lineOff = textAreaLineStartCached(w, ta->scrollRow);
- // Draw gutter background
if (gutterW > 0) {
rectFill(d, ops, gutterX, textY, gutterW, innerH, colors->windowFace);
drawVLine(d, ops, gutterX + gutterW - 1, textY, innerH, colors->windowShadow);
- }
- for (int32_t i = 0; i < visRows; i++) {
- int32_t row = ta->scrollRow + i;
+ for (int32_t i = 0; i < visRows; i++) {
+ int32_t row = ta->te.scrollRow + i;
- if (row >= totalLines) {
- break;
- }
-
- // Use cached line length (O(1) lookup instead of O(lineLen) scan)
- int32_t lineL = textAreaLineLenCached(w, row);
- int32_t drawY = textY + i * font->charHeight;
-
- // Line decorator: background highlight and gutter indicators
- uint32_t lineBg = bg;
- uint32_t gutterColor = 0;
-
- if (ta->lineDecorator) {
- uint32_t decBg = ta->lineDecorator(row + 1, &gutterColor, ta->lineDecoratorCtx);
-
- if (decBg) {
- lineBg = decBg;
+ if (row >= totalLines) {
+ break;
}
- // 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);
- }
+ int32_t drawY = textY + i * font->charHeight;
+ uint32_t gutterColor = 0;
+
+ if (ta->lineDecorator) {
+ ta->lineDecorator(row + 1, &gutterColor, ta->lineDecoratorCtx);
+ }
- // Draw line number in gutter
- if (gutterW > 0) {
- // Gutter indicator (breakpoint dot)
if (gutterColor) {
- // Draw a filled circle using rectFill scanlines.
- // Radius 3 gives a clean 7x7 circle at any font size.
+ // Draw a filled circle as 7 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 = -3; dy <= 3; dy++) {
- int32_t w = hw[dy + 3];
- rectFill(d, ops, cx - w, cy + dy, w * 2 + 1, 1, gutterColor);
+ int32_t hwVal = hw[dy + 3];
+ rectFill(d, ops, cx - hwVal, cy + dy, hwVal * 2 + 1, 1, gutterColor);
}
}
@@ -3038,283 +1603,53 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t numX = gutterX + gutterW - (numLen + 1) * font->charWidth;
drawTextN(d, ops, font, numX, drawY, numBuf, numLen, colors->windowShadow, colors->windowFace, true);
}
-
- // Override bg for this line if the decorator set a custom background.
- // This ensures text character backgrounds match the highlighted line.
- uint32_t savedBg = bg;
- bg = lineBg;
-
- // Expand tabs in this line into a temporary buffer for drawing.
- // Also expand syntax colors so each visual column has a color byte.
- int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3;
-
- // Compute syntax colors for the raw line first (before expansion)
- uint8_t *rawSyntax = ta->rawSyntax;
- char *expandBuf = ta->expandBuf;
- uint8_t *syntaxBuf = ta->syntaxBuf;
- bool hasSyntax = false;
-
- if (ta->colorize && lineL > 0) {
- int32_t colorLen = lineL < MAX_COLORIZE_LEN ? lineL : MAX_COLORIZE_LEN;
- memset(rawSyntax, 0, colorLen);
- ta->colorize(buf + lineOff, colorLen, rawSyntax, ta->colorizeCtx);
- hasSyntax = true;
- }
- int32_t expandLen = 0;
- int32_t vc = 0;
-
- for (int32_t j = 0; j < lineL && expandLen < MAX_COLORIZE_LEN - tabW; j++) {
- if (buf[lineOff + j] == '\t') {
- int32_t spaces = tabW - (vc % tabW);
- uint8_t sc = hasSyntax && j < MAX_COLORIZE_LEN ? rawSyntax[j] : 0;
-
- for (int32_t s = 0; s < spaces && expandLen < MAX_COLORIZE_LEN; s++) {
- expandBuf[expandLen] = ' ';
- syntaxBuf[expandLen] = sc;
- expandLen++;
- vc++;
- }
- } else {
- expandBuf[expandLen] = buf[lineOff + j];
- syntaxBuf[expandLen] = hasSyntax && j < MAX_COLORIZE_LEN ? rawSyntax[j] : 0;
- expandLen++;
- vc++;
- }
- }
-
- // Visible range within expanded line (visual columns)
- int32_t scrollCol = ta->scrollCol;
- int32_t visStart = scrollCol;
- int32_t visEnd = scrollCol + visCols;
- int32_t textEnd = expandLen;
-
- // Clamp visible range to actual expanded content
- int32_t drawStart = visStart < textEnd ? visStart : textEnd;
- int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd;
-
- // Determine selection intersection with this line.
- // Selection offsets are byte-based; convert to visual columns in the expanded buffer.
- int32_t lineSelLo = -1;
- int32_t lineSelHi = -1;
-
- if (selLo >= 0) {
- if (selLo < lineOff + lineL + 1 && selHi > lineOff) {
- int32_t byteSelLo = selLo - lineOff;
- int32_t byteSelHi = selHi - lineOff;
-
- if (byteSelLo < 0) { byteSelLo = 0; }
-
- lineSelLo = visualCol(buf, lineOff, lineOff + byteSelLo, tabW);
- lineSelHi = visualCol(buf, lineOff, lineOff + (byteSelHi < lineL ? byteSelHi : lineL), tabW);
-
- if (byteSelHi > lineL) {
- lineSelHi = expandLen + 1; // extends past EOL
- }
- }
- }
-
- if (lineSelLo >= 0 && lineSelLo < lineSelHi) {
- int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo;
- int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd;
-
- if (vSelLo > vSelHi) { vSelLo = vSelHi; }
-
- // Before selection
- if (drawStart < vSelLo) {
- if (hasSyntax) {
- drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, ta->customSyntaxColors);
- } else {
- drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true);
- }
- }
-
- // Selection (always uses highlight colors, no syntax coloring)
- if (vSelLo < vSelHi) {
- drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, expandBuf + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
- }
-
- // After selection
- if (vSelHi < drawEnd) {
- if (hasSyntax) {
- drawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, ta->customSyntaxColors);
- } else {
- drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true);
- }
- }
-
- // Past end of text: fill selected area with highlight bg
- int32_t nlOff = lineOff + lineL;
- bool pastEolSelected = (nlOff >= selLo && nlOff < selHi);
-
- if (pastEolSelected && drawEnd < visEnd) {
- int32_t selPastStart = drawEnd < lineSelLo ? lineSelLo : drawEnd;
- int32_t selPastEnd = visEnd;
-
- if (selPastStart < visStart) { selPastStart = visStart; }
-
- if (selPastStart < selPastEnd) {
- rectFill(d, ops, textX + (selPastStart - scrollCol) * font->charWidth, drawY, (selPastEnd - selPastStart) * font->charWidth, font->charHeight, colors->menuHighlightBg);
- }
- }
- } else {
- // No selection on this line
- if (drawStart < drawEnd) {
- if (hasSyntax) {
- drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, ta->customSyntaxColors);
- } else {
- drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true);
- }
- }
- }
-
- // Restore bg for next line
- bg = savedBg;
-
- // Advance lineOff to the next line
- lineOff += lineL;
- if (lineOff < len && buf[lineOff] == '\n') {
- lineOff++;
- }
}
- // Draw cursor (blinks at same rate as terminal cursor)
- if (w == sFocusedWidget && sCursorBlinkOn) {
- int32_t curLineOff = textAreaLineStartCached(w, ta->cursorRow);
- int32_t curOff = curLineOff + ta->cursorCol;
+ TextEditPaintHooksT hooks = {
+ .colorize = ta->colorize,
+ .colorizeCtx = ta->colorizeCtx,
+ .lineDecorator = ta->lineDecorator,
+ .lineDecoratorCtx = ta->lineDecoratorCtx,
+ .rawSyntax = ta->rawSyntax,
+ .expandBuf = ta->expandBuf,
+ .syntaxBuf = ta->syntaxBuf,
+ .scratchLen = MAX_COLORIZE_LEN,
+ .customSyntaxColors = ta->customSyntaxColors,
+ };
- if (curOff > len) { curOff = len; }
+ int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3;
+ bool showCursor = (w == sFocusedWidget && sCursorBlinkOn);
- int32_t curVisCol = visualCol(buf, curLineOff, curOff, ta->tabWidth);
- int32_t curDrawCol = curVisCol - ta->scrollCol;
- int32_t curDrawRow = ta->cursorRow - ta->scrollRow;
+ widgetTextEditMultiPaintArea(d, ops, font, colors, &ta->te.lines, ta->te.buf, ta->te.len, textX, textY, innerW, visCols, visRows, ta->te.scrollRow, ta->te.scrollCol, ta->te.cursorRow, ta->te.cursorCol, ta->te.selAnchor, ta->te.selCursor, tabW, fg, bg, showCursor, &hooks);
- if (curDrawCol >= 0 && curDrawCol <= visCols && curDrawRow >= 0 && curDrawRow < visRows) {
- int32_t cursorX = textX + curDrawCol * font->charWidth;
- int32_t cursorY = textY + curDrawRow * font->charHeight;
- drawVLine(d, ops, cursorX, cursorY, font->charHeight, fg);
- }
- }
-
- BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
-
- // Draw vertical scrollbar
if (needVSb) {
int32_t sbX = w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W;
int32_t sbY = w->y + TEXTAREA_BORDER;
- int32_t sbH = innerH;
-
- // Trough
- BevelStyleT troughBevel = BEVEL_TROUGH(colors);
- drawBevel(d, ops, sbX, sbY, TEXTAREA_SB_W, sbH, &troughBevel);
-
- // Up arrow button
- drawBevel(d, ops, sbX, sbY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel);
-
- // Up arrow triangle
- {
- int32_t cx = sbX + TEXTAREA_SB_W / 2;
- int32_t cy = sbY + TEXTAREA_SB_W / 2;
-
- for (int32_t i = 0; i < 4; i++) {
- drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg);
- }
- }
-
- // Down arrow button
- int32_t downY = sbY + sbH - TEXTAREA_SB_W;
- drawBevel(d, ops, sbX, downY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel);
-
- // Down arrow triangle
- {
- int32_t cx = sbX + TEXTAREA_SB_W / 2;
- int32_t cy = downY + TEXTAREA_SB_W / 2;
-
- for (int32_t i = 0; i < 4; i++) {
- drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg);
- }
- }
-
- // Thumb
- int32_t trackLen = sbH - TEXTAREA_SB_W * 2;
-
- if (trackLen > 0) {
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, totalLines, visRows, ta->scrollRow, &thumbPos, &thumbSize);
- drawBevel(d, ops, sbX, sbY + TEXTAREA_SB_W + thumbPos, TEXTAREA_SB_W, thumbSize, &btnBevel);
- }
+ widgetTextScrollbarDraw(d, ops, colors, true, sbX, sbY, TEXTAREA_SB_W, innerH, totalLines, visRows, ta->te.scrollRow);
}
- // Draw horizontal scrollbar
if (needHSb) {
int32_t hsbX = w->x + TEXTAREA_BORDER;
int32_t hsbY = w->y + w->h - TEXTAREA_BORDER - TEXTAREA_SB_W;
int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
+ widgetTextScrollbarDraw(d, ops, colors, false, hsbX, hsbY, TEXTAREA_SB_W, hsbW, maxLL, visCols, ta->te.scrollCol);
- // Trough
- BevelStyleT troughBevel = BEVEL_TROUGH(colors);
- drawBevel(d, ops, hsbX, hsbY, hsbW, TEXTAREA_SB_W, &troughBevel);
-
- // Left arrow button
- drawBevel(d, ops, hsbX, hsbY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel);
-
- // Left arrow triangle
- {
- int32_t cx = hsbX + TEXTAREA_SB_W / 2;
- int32_t cy = hsbY + TEXTAREA_SB_W / 2;
-
- for (int32_t i = 0; i < 4; i++) {
- drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, colors->contentFg);
- }
- }
-
- // Right arrow button
- int32_t rightX = hsbX + hsbW - TEXTAREA_SB_W;
- drawBevel(d, ops, rightX, hsbY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel);
-
- // Right arrow triangle
- {
- int32_t cx = rightX + TEXTAREA_SB_W / 2;
- int32_t cy = hsbY + TEXTAREA_SB_W / 2;
-
- for (int32_t i = 0; i < 4; i++) {
- drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, colors->contentFg);
- }
- }
-
- // Thumb
- int32_t trackLen = hsbW - TEXTAREA_SB_W * 2;
-
- if (trackLen > 0) {
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, maxLL, visCols, ta->scrollCol, &thumbPos, &thumbSize);
- drawBevel(d, ops, hsbX + TEXTAREA_SB_W + thumbPos, hsbY, thumbSize, TEXTAREA_SB_W, &btnBevel);
- }
-
- // Dead corner between scrollbars
if (needVSb) {
int32_t cornerX = w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W;
rectFill(d, ops, cornerX, hsbY, TEXTAREA_SB_W, TEXTAREA_SB_W, colors->windowFace);
}
}
- // Focus rect
if (w == sFocusedWidget) {
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
}
}
-// Handle scrollbar thumb drag for TextArea vertical and horizontal scrollbars.
-// The TextArea always reserves space for the V scrollbar on the right.
-// The H scrollbar appears at the bottom only when the longest line exceeds
-// visible columns.
static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font;
-
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = textAreaGetMaxLineLen(w);
@@ -3331,41 +1666,13 @@ static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
}
if (orient == 0) {
- // Vertical scrollbar drag
int32_t totalLines = textAreaGetLineCount(w);
- int32_t maxScroll = totalLines - visRows;
-
- if (maxScroll <= 0) {
- return;
- }
-
- int32_t trackLen = innerH - TEXTAREA_SB_W * 2;
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, totalLines, visRows, ta->scrollRow, &thumbPos, &thumbSize);
-
- int32_t sbY = w->y + TEXTAREA_BORDER;
- int32_t relMouse = mouseY - sbY - TEXTAREA_SB_W - dragOff;
- int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
- ta->scrollRow = clampInt(newScroll, 0, maxScroll);
+ int32_t sbY = w->y + TEXTAREA_BORDER;
+ ta->te.scrollRow = widgetTextScrollbarDragToScroll(true, mouseY, sbY, TEXTAREA_SB_W, innerH, totalLines, visRows, dragOff);
} else if (orient == 1) {
- // Horizontal scrollbar drag
- int32_t maxHScroll = maxLL - visCols;
-
- if (maxHScroll <= 0) {
- return;
- }
-
- int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
- int32_t trackLen = hsbW - TEXTAREA_SB_W * 2;
- int32_t thumbPos;
- int32_t thumbSize;
- widgetScrollbarThumb(trackLen, maxLL, visCols, ta->scrollCol, &thumbPos, &thumbSize);
-
- int32_t sbX = w->x + TEXTAREA_BORDER;
- int32_t relMouse = mouseX - sbX - TEXTAREA_SB_W - dragOff;
- int32_t newScroll = (trackLen > thumbSize) ? (maxHScroll * relMouse) / (trackLen - thumbSize) : 0;
- ta->scrollCol = clampInt(newScroll, 0, maxHScroll);
+ int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
+ int32_t sbX = w->x + TEXTAREA_BORDER;
+ ta->te.scrollCol = widgetTextScrollbarDragToScroll(false, mouseX, sbX, TEXTAREA_SB_W, hsbW, maxLL, visCols, dragOff);
}
}
@@ -3373,19 +1680,18 @@ static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t d
void widgetTextAreaSetText(WidgetT *w, const char *text) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
- if (ta->buf) {
- strncpy(ta->buf, text, ta->bufSize - 1);
- ta->buf[ta->bufSize - 1] = '\0';
- ta->len = (int32_t)strlen(ta->buf);
- ta->cursorRow = 0;
- ta->cursorCol = 0;
- ta->scrollRow = 0;
- ta->scrollCol = 0;
- ta->desiredCol = 0;
- ta->selAnchor = -1;
- ta->selCursor = -1;
- ta->cachedLines = -1;
- ta->cachedMaxLL = -1;
+ if (ta->te.buf) {
+ strncpy(ta->te.buf, text, ta->te.bufSize - 1);
+ ta->te.buf[ta->te.bufSize - 1] = '\0';
+ ta->te.len = (int32_t)strlen(ta->te.buf);
+ ta->te.cursorRow = 0;
+ ta->te.cursorCol = 0;
+ ta->te.scrollRow = 0;
+ ta->te.scrollCol = 0;
+ ta->te.desiredCol = 0;
+ ta->te.selAnchor = -1;
+ ta->te.selCursor = -1;
+ textAreaDirtyCache(w);
}
}
diff --git a/src/widgets/kpunch/treeView/widgetTreeView.c b/src/widgets/kpunch/treeView/widgetTreeView.c
index cca59df..a252cb1 100644
--- a/src/widgets/kpunch/treeView/widgetTreeView.c
+++ b/src/widgets/kpunch/treeView/widgetTreeView.c
@@ -854,7 +854,24 @@ bool wgtTreeItemIsSelected(const WidgetT *w) {
VALIDATE_WIDGET(w, sTreeItemTypeId, false);
TreeItemDataT *ti = (TreeItemDataT *)w->data;
- return ti->selected;
+
+ // ti->selected tracks multi-select state (Ctrl-click, shift-click
+ // range). In single-select mode, the current selection lives on
+ // the TreeView's tv->selectedItem pointer instead -- walk up to
+ // the owning TreeView and compare, so callers see the expected
+ // answer for both modes.
+ if (ti->selected) {
+ return true;
+ }
+
+ for (WidgetT *p = w->parent; p; p = p->parent) {
+ if (p->type == sTreeViewTypeId) {
+ TreeViewDataT *tv = (TreeViewDataT *)p->data;
+ return tv && tv->selectedItem == w;
+ }
+ }
+
+ return false;
}
@@ -923,17 +940,26 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
TreeViewDataT *tv = (TreeViewDataT *)w->data;
- // Expand all ancestors so the item is visible in the tree
+ // Expand all ancestors so the item is visible in the tree.
+ // Invalidate cached dims so the scroll-into-view below sees the
+ // correct post-expansion totalH / item Y coordinate.
if (item) {
+ bool expandedAny = false;
+
for (WidgetT *p = item->parent; p && p != w; p = p->parent) {
if (p->type == sTreeItemTypeId) {
TreeItemDataT *pti = (TreeItemDataT *)p->data;
if (pti && !pti->expanded) {
pti->expanded = true;
+ expandedAny = true;
}
}
}
+
+ if (expandedAny) {
+ tv->dimsValid = false;
+ }
}
setSelectedItem(w, item);
@@ -945,7 +971,37 @@ void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item) {
tv->anchorItem = item;
}
- wgtInvalidatePaint(w);
+ // Scroll so the selected item is inside the visible band. Manual
+ // expand+click goes through widgetTreeViewOnMouse / OnKey which
+ // have their own scroll-into-view logic; programmatic callers
+ // (e.g. dvxhelp syncing the TOC from navigateToTopic) need this.
+ if (item) {
+ AppContextT *ctx = wgtGetContext(w);
+
+ if (ctx) {
+ const BitmapFontT *font = &ctx->font;
+ int32_t itemY = treeItemYPos(w, item, font);
+
+ if (itemY >= 0) {
+ int32_t totalH;
+ int32_t totalW;
+ int32_t innerH;
+ int32_t innerW;
+ bool needVSb;
+ bool needHSb;
+
+ treeCalcScrollbarNeeds(w, font, &totalH, &totalW, &innerH, &innerW, &needVSb, &needHSb);
+
+ if (itemY < tv->scrollPos) {
+ tv->scrollPos = itemY;
+ } else if (itemY + font->charHeight > tv->scrollPos + innerH) {
+ tv->scrollPos = itemY + font->charHeight - innerH;
+ }
+ }
+ }
+ }
+
+ wgtInvalidate(w);
}
@@ -997,10 +1053,15 @@ static void widgetTreeViewDestroy(WidgetT *w) {
void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font) {
TreeViewDataT *tv = (TreeViewDataT *)w->data;
- // Auto-select first item if nothing is selected
+ // Auto-select first item if nothing is selected. This runs at
+ // layout time (not user action), so set tv->selectedItem directly
+ // instead of going through setSelectedItem -- the latter fires
+ // onChange as if the user clicked, which triggers callers (e.g.
+ // dvxhelp's TOC sync) to re-navigate to the first tree item and
+ // clobber any programmatic selection the host is about to make.
if (!tv->selectedItem) {
WidgetT *first = firstVisibleItem(w);
- setSelectedItem(w, first);
+ tv->selectedItem = first;
if (tv->multiSelect && first) {
((TreeItemDataT *)first->data)->selected = true;