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;