From 5f305dd14cfdef39791e6746af59097eab33e13c Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 13 Apr 2026 20:03:24 -0500 Subject: [PATCH] Bug and performance fixes. Lots of crap I forgot. --- apps/Makefile | 14 +- apps/dvxbasic/Makefile | 6 +- apps/dvxbasic/compiler/lexer.c | 14 + apps/dvxbasic/compiler/lexer.h | 14 + apps/dvxbasic/compiler/opcodes.h | 17 +- apps/dvxbasic/compiler/parser.c | 171 ++++++++++ apps/dvxbasic/formrt/formrt.c | 25 ++ apps/dvxbasic/formrt/formrt.h | 3 + apps/dvxbasic/ide/ideDesigner.c | 14 +- apps/dvxbasic/ide/ideDesigner.h | 2 + apps/dvxbasic/ide/ideMain.c | 274 +++++++++++++++- apps/dvxbasic/ide/ideProject.c | 51 +++ apps/dvxbasic/ide/ideProject.h | 1 + apps/dvxbasic/ide/ideProperties.c | 8 + apps/dvxbasic/runtime/vm.c | 443 ++++++++++++++++++++++++++ apps/progman/progman.c | 52 +-- core/Makefile | 11 +- core/arch.dhs | 4 +- core/dvxApp.c | 108 ++++--- core/dvxTypes.h | 5 +- core/dvxWgt.h | 1 + core/dvxWgtP.h | 4 + core/dvxWm.c | 54 ++++ core/dvxWm.h | 6 + core/platform/dvxPlatformDos.c | 3 + core/widgetCore.c | 7 + core/widgetEvent.c | 50 ++- core/widgetOps.c | 71 ++++- docs/dvx_system_reference.html | 4 +- listhelp/Makefile | 2 +- listhelp/listHelp.c | 127 ++++++-- listhelp/listHelp.h | 18 ++ loader/loaderMain.c | 198 ++++++------ run.sh | 4 +- sdk/readme.txt | 4 +- sdk/samples/hello/makefile | 2 +- sdk/samples/library/makefile | 2 +- sdk/samples/library/mylib.c | 2 +- sdk/samples/widget/makefile | 2 +- sdk/samples/widget/mywgt.c | 2 +- serial/Makefile | 4 +- shell/Makefile | 2 +- sql/Makefile | 4 +- tasks/Makefile | 2 +- texthelp/Makefile | 2 +- widgets/Makefile | 3 +- widgets/canvas/widgetCanvas.c | 22 +- widgets/checkbox/widgetCheckbox.c | 5 +- widgets/comboBox/widgetComboBox.c | 28 +- widgets/dropdown/widgetDropdown.c | 35 +- widgets/listBox/widgetListBox.c | 5 + widgets/listView/widgetListView.c | 28 ++ widgets/radio/widgetRadio.c | 5 +- widgets/scrollPane/widgetScrollPane.c | 19 +- widgets/slider/widgetSlider.c | 4 + widgets/tabControl/widgetTabControl.c | 15 +- widgets/textInput/textInpt.h | 2 + widgets/textInput/widgetTextInput.c | 80 +++-- widgets/treeView/widgetTreeView.c | 57 ++++ 59 files changed, 1815 insertions(+), 307 deletions(-) diff --git a/apps/Makefile b/apps/Makefile index a228a01..0c696b4 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -29,28 +29,28 @@ dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app $(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DXE3GEN) -o $@ -U $< $(DVXRES) build $@ cpanel/cpanel.res $(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DXE3GEN) -o $@ -U $< $(DVXRES) build $@ imgview/imgview.res $(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DXE3GEN) -o $@ -U $< $(BINDIR)/kpunch/notepad/notepad.app: $(OBJDIR)/notepad.o notepad/notepad.res notepad/icon32.bmp | $(BINDIR)/kpunch/notepad - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DXE3GEN) -o $@ -U $< $(DVXRES) build $@ notepad/notepad.res $(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -E _appShutdown -U $< + $(DXE3GEN) -o $@ -U $< $(DVXRES) build $@ clock/clock.res DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp $(BINDIR)/kpunch/dvxdemo/dvxdemo.app: $(OBJDIR)/dvxdemo.o $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) dvxdemo/dvxdemo.res dvxdemo/icon32.bmp | $(BINDIR)/kpunch/dvxdemo - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DXE3GEN) -o $@ -U $< $(DVXRES) build $@ dvxdemo/dvxdemo.res cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/ @@ -73,7 +73,7 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $< + $(DXE3GEN) -o $@ -U $< $(DVXRES) build $@ dvxhelp/dvxhelp.res $(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR) diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index 5c80484..04ca477 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -71,9 +71,7 @@ $(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR) # Runtime library DXE (exports symbols via dlregsym constructor) $(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR) - $(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe \ - -E _bas -E _BAS \ - -U $(RT_OBJS) + $(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS) mv $(RT_TARGETDIR)/basrt.dxe $@ $(RT_TARGETDIR)/basrt.dep: ../../config/basrt.dep | $(RT_TARGETDIR) @@ -81,7 +79,7 @@ $(RT_TARGETDIR)/basrt.dep: ../../config/basrt.dep | $(RT_TARGETDIR) # IDE app DXE (compiler linked in, runtime from basrt.lib) $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR) - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $(COMP_OBJS) $(APP_OBJS) + $(DXE3GEN) -o $@ -U $(COMP_OBJS) $(APP_OBJS) $(DVXRES) build $@ dvxbasic.res diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index 5e776fa..c214462 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -32,7 +32,11 @@ static const KeywordEntryT sKeywords[] = { { "BYVAL", TOK_BYVAL }, { "CALL", TOK_CALL }, { "CASE", TOK_CASE }, + { "CHDIR", TOK_CHDIR }, + { "CHDRIVE", TOK_CHDRIVE }, { "CLOSE", TOK_CLOSE }, + { "CURDIR", TOK_CURDIR }, + { "CURDIR$", TOK_CURDIR }, { "CONST", TOK_CONST }, { "DATA", TOK_DATA }, { "DECLARE", TOK_DECLARE }, @@ -43,6 +47,8 @@ static const KeywordEntryT sKeywords[] = { { "DEFSNG", TOK_DEFSNG }, { "DEFSTR", TOK_DEFSTR }, { "DIM", TOK_DIM }, + { "DIR", TOK_DIR }, + { "DIR$", TOK_DIR }, { "DO", TOK_DO }, { "DOEVENTS", TOK_DOEVENTS }, { "DOUBLE", TOK_DOUBLE }, @@ -57,9 +63,12 @@ static const KeywordEntryT sKeywords[] = { { "EXPLICIT", TOK_EXPLICIT }, { "EXIT", TOK_EXIT }, { "FALSE", TOK_FALSE_KW }, + { "FILECOPY", TOK_FILECOPY }, + { "FILELEN", TOK_FILELEN }, { "FOR", TOK_FOR }, { "FUNCTION", TOK_FUNCTION }, { "GET", TOK_GET }, + { "GETATTR", TOK_GETATTR }, { "GOSUB", TOK_GOSUB }, { "GOTO", TOK_GOTO }, { "HIDE", TOK_HIDE }, @@ -70,6 +79,7 @@ static const KeywordEntryT sKeywords[] = { { "INPUT", TOK_INPUT }, { "INTEGER", TOK_INTEGER }, { "IS", TOK_IS }, + { "KILL", TOK_KILL }, { "LBOUND", TOK_LBOUND }, { "LET", TOK_LET }, { "LINE", TOK_LINE }, @@ -77,10 +87,12 @@ static const KeywordEntryT sKeywords[] = { { "LONG", TOK_LONG }, { "LOOP", TOK_LOOP }, { "ME", TOK_ME }, + { "MKDIR", TOK_MKDIR }, { "MOD", TOK_MOD }, { "INPUTBOX", TOK_INPUTBOX }, { "INPUTBOX$", TOK_INPUTBOX }, { "MSGBOX", TOK_MSGBOX }, + { "NAME", TOK_NAME }, { "NEXT", TOK_NEXT }, { "NOT", TOK_NOT }, { "ON", TOK_ON }, @@ -99,9 +111,11 @@ static const KeywordEntryT sKeywords[] = { { "RESTORE", TOK_RESTORE }, { "RESUME", TOK_RESUME }, { "RETURN", TOK_RETURN }, + { "RMDIR", TOK_RMDIR }, { "SEEK", TOK_SEEK }, { "SELECT", TOK_SELECT }, { "SET", TOK_SET }, + { "SETATTR", TOK_SETATTR }, { "SHARED", TOK_SHARED }, { "SHELL", TOK_SHELL }, { "SHOW", TOK_SHOW }, diff --git a/apps/dvxbasic/compiler/lexer.h b/apps/dvxbasic/compiler/lexer.h index 158d662..d76b625 100644 --- a/apps/dvxbasic/compiler/lexer.h +++ b/apps/dvxbasic/compiler/lexer.h @@ -169,6 +169,20 @@ typedef enum { TOK_WRITE, TOK_XOR, + // Filesystem keywords + TOK_CHDIR, + TOK_CHDRIVE, + TOK_CURDIR, + TOK_DIR, + TOK_FILECOPY, + TOK_FILELEN, + TOK_GETATTR, + TOK_KILL, + TOK_MKDIR, + TOK_NAME, + TOK_RMDIR, + TOK_SETATTR, + // File modes TOK_APPEND, TOK_BINARY, diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index c2960ce..5cfccb7 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -342,8 +342,23 @@ #define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string #define OP_INI_WRITE 0xE1 // pop value, pop key, pop section, pop file +// Filesystem operations +#define OP_FS_KILL 0xE2 // pop filename, delete file +#define OP_FS_NAME 0xE3 // pop newname, pop oldname, rename +#define OP_FS_FILECOPY 0xE4 // pop dst, pop src, copy file +#define OP_FS_MKDIR 0xE5 // pop path, create directory +#define OP_FS_RMDIR 0xE6 // pop path, remove directory +#define OP_FS_CHDIR 0xE7 // pop path, change directory +#define OP_FS_CHDRIVE 0xE8 // pop drive, change drive +#define OP_FS_CURDIR 0xE9 // push current directory string +#define OP_FS_DIR 0xEA // pop pattern, push first matching filename +#define OP_FS_DIR_NEXT 0xEB // push next matching filename (no args) +#define OP_FS_FILELEN 0xEC // pop filename, push file length +#define OP_FS_GETATTR 0xED // pop filename, push attributes integer +#define OP_FS_SETATTR 0xEE // pop attrs, pop filename, set attributes + // Debug -#define OP_LINE 0xE2 // [uint16 lineNum] set current source line for debugger +#define OP_LINE 0xEF // [uint16 lineNum] set current source line for debugger // ============================================================ // Halt diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 51ebae8..fb1a9e5 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -141,6 +141,8 @@ static void parsePrimary(BasParserT *p); static void parseAssignOrCall(BasParserT *p); static void parseBeginForm(BasParserT *p); +static void parseChDir(BasParserT *p); +static void parseChDrive(BasParserT *p); static void parseClose(BasParserT *p); static void parseConst(BasParserT *p); static void parseData(BasParserT *p); @@ -154,6 +156,7 @@ static void parseEnd(BasParserT *p); static void parseEndForm(BasParserT *p); static void parseErase(BasParserT *p); static void parseExit(BasParserT *p); +static void parseFileCopy(BasParserT *p); static void parseFor(BasParserT *p); static void parseFunction(BasParserT *p); static void parseGet(BasParserT *p); @@ -161,8 +164,11 @@ static void parseGosub(BasParserT *p); static void parseGoto(BasParserT *p); static void parseIf(BasParserT *p); static void parseInput(BasParserT *p); +static void parseKill(BasParserT *p); static void parseLineInput(BasParserT *p); +static void parseMkDir(BasParserT *p); static void parseModule(BasParserT *p); +static void parseName(BasParserT *p); static void parseOn(BasParserT *p); static void parseOnError(BasParserT *p); static void parseOpen(BasParserT *p); @@ -173,7 +179,9 @@ static void parseRead(BasParserT *p); static void parseRedim(BasParserT *p); static void parseRestore(BasParserT *p); static void parseResume(BasParserT *p); +static void parseRmDir(BasParserT *p); static void parseSeek(BasParserT *p); +static void parseSetAttr(BasParserT *p); static void parseSelectCase(BasParserT *p); static void parseShell(BasParserT *p); static void parseSleep(BasParserT *p); @@ -250,6 +258,14 @@ static void addPredefConsts(BasParserT *p) { // Show mode flags addPredefConst(p, "vbModal", 1); + + // File attribute constants + addPredefConst(p, "vbNormal", 0); + addPredefConst(p, "vbReadOnly", 1); + addPredefConst(p, "vbHidden", 2); + addPredefConst(p, "vbSystem", 4); + addPredefConst(p, "vbDirectory", 16); + addPredefConst(p, "vbArchive", 32); } @@ -1304,6 +1320,59 @@ static void parsePrimary(BasParserT *p) { return; } + // CurDir$ -- current directory (no args) + if (tt == TOK_CURDIR) { + advance(p); + if (check(p, TOK_LPAREN)) { + expect(p, TOK_LPAREN); + expect(p, TOK_RPAREN); + } + basEmit8(&p->cg, OP_FS_CURDIR); + return; + } + + // Dir$(pattern) or Dir$() for next match + if (tt == TOK_DIR) { + advance(p); + if (check(p, TOK_LPAREN)) { + expect(p, TOK_LPAREN); + if (check(p, TOK_RPAREN)) { + // Dir$() -- no args, get next match + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FS_DIR_NEXT); + } else { + // Dir$(pattern) + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FS_DIR); + } + } else { + // Dir with no parens -- next match + basEmit8(&p->cg, OP_FS_DIR_NEXT); + } + return; + } + + // FileLen(filename) -- file size without opening + if (tt == TOK_FILELEN) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FS_FILELEN); + return; + } + + // GetAttr(filename) -- file attributes + if (tt == TOK_GETATTR) { + advance(p); + expect(p, TOK_LPAREN); + parseExpression(p); + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_FS_GETATTR); + return; + } + // InputBox$(prompt [, title [, default]]) if (tt == TOK_INPUTBOX) { advance(p); @@ -4563,6 +4632,76 @@ static void parseSelectCase(BasParserT *p) { } +static void parseChDir(BasParserT *p) { + // CHDIR path + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_FS_CHDIR); +} + + +static void parseChDrive(BasParserT *p) { + // CHDRIVE drive + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_FS_CHDRIVE); +} + + +static void parseFileCopy(BasParserT *p) { + // FILECOPY source, dest + advance(p); + parseExpression(p); + expect(p, TOK_COMMA); + parseExpression(p); + basEmit8(&p->cg, OP_FS_FILECOPY); +} + + +static void parseKill(BasParserT *p) { + // KILL filename + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_FS_KILL); +} + + +static void parseMkDir(BasParserT *p) { + // MKDIR path + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_FS_MKDIR); +} + + +static void parseName(BasParserT *p) { + // NAME oldname AS newname + advance(p); + parseExpression(p); + expect(p, TOK_AS); + parseExpression(p); + basEmit8(&p->cg, OP_FS_NAME); +} + + +static void parseRmDir(BasParserT *p) { + // RMDIR path + advance(p); + parseExpression(p); + basEmit8(&p->cg, OP_FS_RMDIR); +} + + +static void parseSetAttr(BasParserT *p) { + // SETATTR filename, attributes + advance(p); + parseExpression(p); + expect(p, TOK_COMMA); + parseExpression(p); + basEmit8(&p->cg, OP_FS_SETATTR); +} + + static void parseShell(BasParserT *p) { // SHELL "command" -- execute an OS command (discard return value) // SHELL -- no argument, no-op in embedded context @@ -4793,14 +4932,38 @@ static void parseStatement(BasParserT *p) { parseRedim(p); break; + case TOK_FILECOPY: + parseFileCopy(p); + break; + case TOK_INPUT: parseInput(p); break; + case TOK_KILL: + parseKill(p); + break; + + case TOK_MKDIR: + parseMkDir(p); + break; + + case TOK_NAME: + parseName(p); + break; + case TOK_OPEN: parseOpen(p); break; + case TOK_CHDIR: + parseChDir(p); + break; + + case TOK_CHDRIVE: + parseChDrive(p); + break; + case TOK_CLOSE: parseClose(p); break; @@ -4849,6 +5012,14 @@ static void parseStatement(BasParserT *p) { parseResume(p); break; + case TOK_RMDIR: + parseRmDir(p); + break; + + case TOK_SETATTR: + parseSetAttr(p); + break; + case TOK_RETURN: advance(p); if (p->sym.inLocalScope) { diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index e3e148e..54527c0 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -624,6 +624,11 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { return basValStringFromC(text ? text : ""); } + // Help topic + if (strcasecmp(propName, "HelpTopic") == 0) { + return basValStringFromC(ctrl->helpTopic); + } + // Data binding properties if (strcasecmp(propName, "DataSource") == 0) { return basValStringFromC(ctrl->dataSource); @@ -1071,6 +1076,16 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen continue; } + // HelpTopic is stored on BasControlT, not on the widget + if (strcasecmp(key, "HelpTopic") == 0) { + char *text = value; + if (text[0] == '"') { text++; } + int32_t tlen = (int32_t)strlen(text); + if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; } + snprintf(current->helpTopic, sizeof(current->helpTopic), "%s", text); + continue; + } + // Layout property on a container: replace the parentStack entry // with a layout box inside the container widget. // NOTE: only do this for non-default layouts (HBox, WrapBox). @@ -1134,6 +1149,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen form->frmAutoSize = (strcasecmp(text, "True") == 0); } else if (strcasecmp(key, "Layout") == 0) { snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text); + } else if (strcasecmp(key, "HelpTopic") == 0) { + snprintf(form->helpTopic, sizeof(form->helpTopic), "%s", text); } } } @@ -1453,6 +1470,14 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } + // Help topic + if (strcasecmp(propName, "HelpTopic") == 0) { + BasStringT *s = basValFormatString(value); + snprintf(ctrl->helpTopic, BAS_MAX_CTRL_NAME, "%s", s->data); + basStringUnref(s); + return; + } + // Data binding properties (stored on BasControlT, not on the widget) if (strcasecmp(propName, "DataSource") == 0) { BasStringT *s = basValFormatString(value); diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index 125385d..837dce3 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -54,6 +54,7 @@ typedef struct BasControlT { char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding char dataField[BAS_MAX_CTRL_NAME]; // column name for binding + char helpTopic[BAS_MAX_CTRL_NAME]; // help topic ID for F1 int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls) } BasControlT; @@ -81,6 +82,7 @@ typedef struct BasFormT { bool frmCentered; bool frmAutoSize; char frmLayout[32]; // "VBox", "HBox", or "WrapBox" + char helpTopic[BAS_MAX_CTRL_NAME]; // form-level help topic // Per-form variable storage (allocated at load, freed at unload) BasValueT *formVars; int32_t formVarCount; @@ -110,6 +112,7 @@ typedef struct { BasFormT **forms; // stb_ds array of heap-allocated pointers int32_t formCount; BasFormT *currentForm; // form currently dispatching events + char helpFile[256]; // project help file path (for F1) BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources int32_t frmCacheCount; } BasFormRtT; diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index c663f87..9b5053d 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -749,8 +749,9 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { else if (strcasecmp(key, "MaxWidth") == 0) { curCtrl->maxWidth = atoi(val); } else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); } else if (strcasecmp(key, "Weight") == 0) { curCtrl->weight = atoi(val); } - else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); } - else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ } + else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); } + else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(curCtrl->helpTopic, DSGN_MAX_NAME, "%s", val); } + else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ } else { setPropValue(curCtrl, key, val); } } else { if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); } @@ -762,6 +763,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { else if (strcasecmp(key, "Top") == 0) { form->top = atoi(val); } else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); form->autoSize = false; } else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); form->autoSize = false; } + else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", val); } } } } @@ -1140,6 +1142,10 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i pos += snprintf(buf + pos, bufSize - pos, "%s Weight = %d\n", pad, (int)ctrl->weight); } + if (ctrl->helpTopic[0]) { + pos += snprintf(buf + pos, bufSize - pos, "%s HelpTopic = \"%s\"\n", pad, ctrl->helpTopic); + } + for (int32_t j = 0; j < ctrl->propCount; j++) { if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; } if (strcasecmp(ctrl->props[j].name, "Text") == 0) { continue; } @@ -1236,6 +1242,10 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) { pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height); } + if (ds->form->helpTopic[0]) { + pos += snprintf(buf + pos, bufSize - pos, " HelpTopic = \"%s\"\n", ds->form->helpTopic); + } + // Output menu items as nested Begin Menu blocks { int32_t menuCount = (int32_t)arrlen(ds->form->menuItems); diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index 44ab578..84417f7 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -66,6 +66,7 @@ typedef struct { int32_t maxWidth; // 0 = no cap (stretch to fill) int32_t maxHeight; // 0 = no cap (stretch to fill) int32_t weight; // layout weight (0 = fixed size, >0 = share extra space) + char helpTopic[DSGN_MAX_NAME]; // help topic ID for F1 DsgnPropT props[DSGN_MAX_PROPS]; int32_t propCount; WidgetT *widget; // live widget (created at design time for WYSIWYG) @@ -86,6 +87,7 @@ typedef struct { bool centered; // true = center on screen, false = use left/top bool autoSize; // true = dvxFitWindow, false = use width/height bool resizable; // true = user can resize at runtime + char helpTopic[DSGN_MAX_NAME]; // form-level help topic DsgnControlT **controls; // stb_ds array of heap-allocated pointers DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus) bool dirty; diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 452ae2c..389d4b9 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -23,6 +23,10 @@ #include "radio/radio.h" #include "textInput/textInpt.h" #include "dropdown/dropdown.h" +#include "canvas/canvas.h" +#include "listBox/listBox.h" +#include "slider/slider.h" +#include "tabControl/tabCtrl.h" #include "button/button.h" #include "splitter/splitter.h" #include "statusBar/statBar.h" @@ -470,6 +474,7 @@ static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize) // ============================================================ static DxeAppContextT *sCtx = NULL; +static char sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run) static AppContextT *sAc = NULL; static PrefsHandleT *sPrefs = NULL; static WindowT *sWin = NULL; // Main toolbar window @@ -794,6 +799,7 @@ int32_t appMain(DxeAppContextT *ctx) { // Set help file and context-sensitive F1 handler snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, "dvxbasic.hlp"); + snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile); ctx->onHelpQuery = helpQueryHandler; ctx->helpQueryCtx = NULL; @@ -2171,6 +2177,12 @@ static void runModule(BasModuleT *mod) { sDbgFormRt = formRt; sDbgModule = mod; sDbgState = DBG_RUNNING; + + // Set project help file on form runtime for F1 context help + if (sProject.helpFile[0]) { + snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s%c%s", + sProject.projectDir, DVX_PATH_SEP, sProject.helpFile); + } updateProjectMenuState(); // Set breakpoints BEFORE loading forms so breakpoints in form @@ -2206,7 +2218,6 @@ static void runModule(BasModuleT *mod) { // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, IDE_STEP_SLICE); - int32_t totalSteps = 0; BasVmResultE result; sStopRequested = false; @@ -2228,7 +2239,6 @@ static void runModule(BasModuleT *mod) { } result = basVmRun(vm); - totalSteps += vm->stepCount; if (result == BAS_VM_BREAKPOINT) { sDbgState = DBG_PAUSED; @@ -2278,8 +2288,6 @@ static void runModule(BasModuleT *mod) { sStopRequested = false; while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) { - totalSteps += vm->stepCount; - vm->stepCount = 0; if (sDbgState == DBG_PAUSED) { // Paused inside an event handler @@ -2321,9 +2329,7 @@ static void runModule(BasModuleT *mod) { updateProjectMenuState(); setOutputText(sOutputBuf); - static char statusBuf[128]; - snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps); - setStatus(statusBuf); + setStatus("Done."); // Restore IDE windows if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); } @@ -4691,9 +4697,34 @@ static void handleViewCmd(int32_t cmd) { // Preferences dialog // ============================================================ +// Color entry names for the Colors tab (matches SYNTAX_* indices) +static const char *sSyntaxColorNames[] = { + "Default Text", // 0 = SYNTAX_DEFAULT + "Keywords", // 1 = SYNTAX_KEYWORD + "Strings", // 2 = SYNTAX_STRING + "Comments", // 3 = SYNTAX_COMMENT + "Numbers", // 4 = SYNTAX_NUMBER + "Operators", // 5 = SYNTAX_OPERATOR + "Types", // 6 = SYNTAX_TYPE +}; + +#define SYNTAX_COLOR_COUNT 7 + +// Default syntax colors (0x00RRGGBB; 0 = use widget default) +static const uint32_t sDefaultSyntaxColors[SYNTAX_COLOR_COUNT] = { + 0x00000000, // default -- not used (widget fg) + 0x00000080, // keyword -- dark blue + 0x00800000, // string -- dark red + 0x00008000, // comment -- dark green + 0x00800080, // number -- purple + 0x00808000, // operator -- dark yellow + 0x00008080, // type -- teal +}; + static struct { bool done; bool accepted; + // General tab WidgetT *renameSkipComments; WidgetT *optionExplicit; WidgetT *tabWidthInput; @@ -4703,6 +4734,16 @@ static struct { WidgetT *defVersion; WidgetT *defCopyright; WidgetT *defDescription; + // Colors tab + WidgetT *colorList; + WidgetT *sliderR; + WidgetT *sliderG; + WidgetT *sliderB; + WidgetT *lblR; + WidgetT *lblG; + WidgetT *lblB; + WidgetT *colorSwatch; + uint32_t syntaxColors[SYNTAX_COLOR_COUNT]; } sPrefsDlg; @@ -4719,10 +4760,97 @@ static void onPrefsCancel(WidgetT *w) { } +static void prefsUpdateSwatch(void) { + if (!sPrefsDlg.colorSwatch) { + return; + } + + uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR); + uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG); + uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB); + + uint32_t color = packColor(&sAc->display, r, g, b); + wgtCanvasClear(sPrefsDlg.colorSwatch, color); +} + + +static void prefsUpdateColorSliders(void) { + int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList); + + if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) { + return; + } + + uint32_t c = sPrefsDlg.syntaxColors[idx]; + uint8_t r = (c >> 16) & 0xFF; + uint8_t g = (c >> 8) & 0xFF; + uint8_t b = c & 0xFF; + + wgtSliderSetValue(sPrefsDlg.sliderR, r); + wgtSliderSetValue(sPrefsDlg.sliderG, g); + wgtSliderSetValue(sPrefsDlg.sliderB, b); + + static char rBuf[8]; + static char gBuf[8]; + static char bBuf[8]; + snprintf(rBuf, sizeof(rBuf), "%d", (int)r); + snprintf(gBuf, sizeof(gBuf), "%d", (int)g); + snprintf(bBuf, sizeof(bBuf), "%d", (int)b); + wgtSetText(sPrefsDlg.lblR, rBuf); + wgtSetText(sPrefsDlg.lblG, gBuf); + wgtSetText(sPrefsDlg.lblB, bBuf); + + prefsUpdateSwatch(); +} + + +static void onColorListChange(WidgetT *w) { + (void)w; + prefsUpdateColorSliders(); +} + + +static void onColorSliderChange(WidgetT *w) { + (void)w; + + int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList); + + if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) { + return; + } + + uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR); + uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG); + uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB); + + static char rBuf[8]; + static char gBuf[8]; + static char bBuf[8]; + snprintf(rBuf, sizeof(rBuf), "%d", (int)r); + snprintf(gBuf, sizeof(gBuf), "%d", (int)g); + snprintf(bBuf, sizeof(bBuf), "%d", (int)b); + wgtSetText(sPrefsDlg.lblR, rBuf); + wgtSetText(sPrefsDlg.lblG, gBuf); + wgtSetText(sPrefsDlg.lblB, bBuf); + + sPrefsDlg.syntaxColors[idx] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; + prefsUpdateSwatch(); +} + + +static void applySyntaxColors(void) { + if (!sEditor) { + return; + } + + wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, SYNTAX_COLOR_COUNT); +} + + static void showPreferencesDialog(void) { memset(&sPrefsDlg, 0, sizeof(sPrefsDlg)); - WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 400, 440, false); + WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 420, 440, false); if (!win) { return; @@ -4734,8 +4862,16 @@ static void showPreferencesDialog(void) { WidgetT *root = wgtInitWindow(sAc, win); root->spacing = wgtPixels(4); - // ---- Editor section ---- - WidgetT *edFrame = wgtFrame(root, "Editor"); + // ---- Tab control ---- + WidgetT *tabs = wgtTabControl(root); + tabs->weight = 100; + + // ======== General tab ======== + WidgetT *generalPage = wgtTabPage(tabs, "General"); + generalPage->spacing = wgtPixels(4); + + // Editor section + WidgetT *edFrame = wgtFrame(generalPage, "Editor"); edFrame->spacing = wgtPixels(2); sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming"); @@ -4757,9 +4893,10 @@ static void showPreferencesDialog(void) { sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs"); wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true)); - // ---- Project Defaults section ---- - WidgetT *prjFrame = wgtFrame(root, "New Project Defaults"); + // Project Defaults section + WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults"); prjFrame->spacing = wgtPixels(2); + prjFrame->weight = 100; WidgetT *r1 = wgtHBox(prjFrame); r1->spacing = wgtPixels(4); @@ -4799,6 +4936,60 @@ static void showPreferencesDialog(void) { sPrefsDlg.defDescription->minH = wgtPixels(48); wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", "")); + // ======== Colors tab ======== + WidgetT *colorsPage = wgtTabPage(tabs, "Colors"); + colorsPage->spacing = wgtPixels(4); + + // Load current colors from prefs (or defaults) + for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) { + char key[32]; + snprintf(key, sizeof(key), "color%d", (int)i); + sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]); + } + + WidgetT *colorsHBox = wgtHBox(colorsPage); + colorsHBox->spacing = wgtPixels(8); + colorsHBox->weight = 100; + + // Left: color list + sPrefsDlg.colorList = wgtListBox(colorsHBox); + sPrefsDlg.colorList->weight = 100; + sPrefsDlg.colorList->onChange = onColorListChange; + + wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, SYNTAX_COLOR_COUNT); + + // Right: RGB sliders + value labels + swatch preview + WidgetT *sliderBox = wgtVBox(colorsHBox); + sliderBox->spacing = wgtPixels(2); + sliderBox->weight = 100; + + wgtLabel(sliderBox, "Red:"); + sPrefsDlg.sliderR = wgtSlider(sliderBox, 0, 255); + sPrefsDlg.sliderR->onChange = onColorSliderChange; + sPrefsDlg.lblR = wgtLabel(sliderBox, "0"); + wgtLabelSetAlign(sPrefsDlg.lblR, AlignEndE); + + wgtLabel(sliderBox, "Green:"); + sPrefsDlg.sliderG = wgtSlider(sliderBox, 0, 255); + sPrefsDlg.sliderG->onChange = onColorSliderChange; + sPrefsDlg.lblG = wgtLabel(sliderBox, "0"); + wgtLabelSetAlign(sPrefsDlg.lblG, AlignEndE); + + wgtLabel(sliderBox, "Blue:"); + sPrefsDlg.sliderB = wgtSlider(sliderBox, 0, 255); + sPrefsDlg.sliderB->onChange = onColorSliderChange; + sPrefsDlg.lblB = wgtLabel(sliderBox, "0"); + wgtLabelSetAlign(sPrefsDlg.lblB, AlignEndE); + + wgtLabel(sliderBox, "Preview:"); + sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, 64, 24); + + // Select first color entry and load sliders + wgtListBoxSetSelected(sPrefsDlg.colorList, 1); + prefsUpdateColorSliders(); + + wgtTabControlSetActive(tabs, 0); + // ---- OK / Cancel ---- WidgetT *btnRow = wgtHBox(root); btnRow->spacing = wgtPixels(8); @@ -4822,6 +5013,7 @@ static void showPreferencesDialog(void) { } if (sPrefsDlg.accepted) { + // General tab prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments)); prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit)); prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces)); @@ -4856,6 +5048,14 @@ static void showPreferencesDialog(void) { val = wgtGetText(sPrefsDlg.defDescription); prefsSetString(sPrefs, "defaults", "description", val ? val : ""); + // Colors tab + for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) { + char key[32]; + snprintf(key, sizeof(key), "color%d", (int)i); + prefsSetInt(sPrefs, "syntax", key, (int32_t)sPrefsDlg.syntaxColors[i]); + } + + applySyntaxColors(); prefsSave(sPrefs); } @@ -4959,12 +5159,48 @@ static void helpQueryHandler(void *ctx) { sCtx->helpTopic[0] = '\0'; + // Restore IDE help file (may have been swapped for a BASIC program's) + snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sIdeHelpFile); + // Determine which window is focused WindowT *focusWin = NULL; if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) { focusWin = sAc->stack.windows[sAc->stack.focusedIdx]; } + // Running BASIC program: check if focused window belongs to a form + if (sDbgFormRt && sDbgFormRt->helpFile[0] && focusWin) { + for (int32_t i = 0; i < sDbgFormRt->formCount; i++) { + BasFormT *form = sDbgFormRt->forms[i]; + + if (form->window == focusWin) { + // Swap to the project's help file + snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sDbgFormRt->helpFile); + + // Find the focused widget's HelpTopic + WidgetT *focused = wgtGetFocused(); + + if (focused && focused->userData) { + BasControlT *ctrl = (BasControlT *)focused->userData; + + if (ctrl->helpTopic[0]) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", ctrl->helpTopic); + return; + } + } + + // Fall back to form-level HelpTopic + if (form->helpTopic[0]) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", form->helpTopic); + return; + } + + // No topic set -- open help at default + return; + } + } + } + // Code editor: look up the word under the cursor if (focusWin == sCodeWin && sEditor) { char word[128]; @@ -6893,6 +7129,20 @@ static void showCodeWindow(void) { sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE); sEditor->weight = 100; wgtTextAreaSetColorize(sEditor, basicColorize, NULL); + + // Apply saved syntax colors + { + uint32_t initColors[SYNTAX_COLOR_COUNT]; + + for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) { + char key[32]; + snprintf(key, sizeof(key), "color%d", (int)i); + initColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]); + } + + wgtTextAreaSetSyntaxColors(sEditor, initColors, SYNTAX_COLOR_COUNT); + } + wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc); wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetShowLineNumbers(sEditor, true); diff --git a/apps/dvxbasic/ide/ideProject.c b/apps/dvxbasic/ide/ideProject.c index 3230ec9..6f0e54f 100644 --- a/apps/dvxbasic/ide/ideProject.c +++ b/apps/dvxbasic/ide/ideProject.c @@ -148,6 +148,9 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) { val = prefsGetString(h, "Project", "Icon", NULL); if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); } + val = prefsGetString(h, "Project", "HelpFile", NULL); + if (val) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", val); } + // [Modules] section -- File0, File1, ... for (int32_t i = 0; i < PRJ_MAX_FILES; i++) { char key[16]; @@ -208,6 +211,7 @@ bool prjSave(const PrjStateT *prj) { if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); } if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); } if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); } + if (prj->helpFile[0]) { prefsSetString(h, "Project", "HelpFile", prj->helpFile); } // [Modules] section int32_t modIdx = 0; @@ -620,6 +624,7 @@ static struct { WidgetT *description; WidgetT *startupForm; const char **formNames; // stb_ds array of form name strings for startup dropdown + WidgetT *helpFileInput; WidgetT *iconPreview; char iconPath[DVX_MAX_PATH]; const char *appPath; @@ -801,6 +806,33 @@ static void ppdOnBrowseIcon(WidgetT *w) { } +static void ppdOnBrowseHelp(WidgetT *w) { + (void)w; + + FileFilterT filters[] = { + { "Help Files (*.hlp)", "*.hlp" }, + { "All Files (*.*)", "*.*" } + }; + + char path[DVX_MAX_PATH]; + + if (!dvxFileDialog(sPpd.ctx, "Select Help File", FD_SAVE, NULL, filters, 2, path, sizeof(path))) { + return; + } + + // Convert to project-relative path + const char *relPath = path; + int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir); + + if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 && + (path[dirLen] == '/' || path[dirLen] == '\\')) { + relPath = path + dirLen + 1; + } + + wgtSetText(sPpd.helpFileInput, relPath); +} + + static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) { WidgetT *row = wgtHBox(parent); row->spacing = wgtPixels(4); @@ -927,6 +959,22 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) ppdLoadIconPreview(); } + // Help file row + { + WidgetT *hlpRow = wgtHBox(root); + hlpRow->spacing = wgtPixels(4); + + WidgetT *hlpLbl = wgtLabel(hlpRow, "Help File:"); + hlpLbl->minW = wgtPixels(PPD_LABEL_W); + + sPpd.helpFileInput = wgtTextInput(hlpRow, DVX_MAX_PATH); + sPpd.helpFileInput->weight = 100; + wgtSetText(sPpd.helpFileInput, prj->helpFile); + + WidgetT *hlpBrowse = wgtButton(hlpRow, "Browse..."); + hlpBrowse->onClick = ppdOnBrowseHelp; + } + // Description: label above, textarea below (matches Preferences layout) wgtLabel(root, "Description:"); sPpd.description = wgtTextArea(root, PRJ_MAX_DESC); @@ -980,6 +1028,9 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath); + s = wgtGetText(sPpd.helpFileInput); + if (s) { snprintf(prj->helpFile, sizeof(prj->helpFile), "%s", s); } + // Read startup form from dropdown if (sPpd.startupForm && sPpd.formNames) { int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm); diff --git a/apps/dvxbasic/ide/ideProject.h b/apps/dvxbasic/ide/ideProject.h index 8dea942..0a9d300 100644 --- a/apps/dvxbasic/ide/ideProject.h +++ b/apps/dvxbasic/ide/ideProject.h @@ -56,6 +56,7 @@ typedef struct { char copyright[PRJ_MAX_STRING]; char description[PRJ_MAX_DESC]; char iconPath[DVX_MAX_PATH]; // relative path to icon BMP + char helpFile[DVX_MAX_PATH]; // relative path to .hlp file PrjFileT *files; // stb_ds dynamic array int32_t fileCount; PrjSourceMapT *sourceMap; // stb_ds dynamic array diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c index 43a3e96..76c340d 100644 --- a/apps/dvxbasic/ide/ideProperties.c +++ b/apps/dvxbasic/ide/ideProperties.c @@ -319,6 +319,7 @@ static uint8_t getPropType(const char *propName, const char *typeName) { if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; } + if (strcasecmp(propName, "HelpTopic") == 0) { return PROP_TYPE_STRING; } if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; } if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } @@ -828,6 +829,8 @@ static void onPropDblClick(WidgetT *w) { bool vis = ctrl->widget ? ctrl->widget->visible : true; cascadeToChildren(sDs, ctrl->name, vis, val); } + } else if (strcasecmp(propName, "HelpTopic") == 0) { + snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue); } else { // Try widget iface setter first bool ifaceHandled = false; @@ -976,6 +979,8 @@ static void onPropDblClick(WidgetT *w) { } else if (strcasecmp(propName, "Height") == 0) { sDs->form->height = atoi(newValue); sDs->form->autoSize = false; + } else if (strcasecmp(propName, "HelpTopic") == 0) { + snprintf(sDs->form->helpTopic, DSGN_MAX_NAME, "%s", newValue); } sDs->form->dirty = true; @@ -1437,6 +1442,7 @@ void prpRefresh(DsgnStateT *ds) { addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False"); addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False"); + addPropRow("HelpTopic", ctrl->helpTopic); for (int32_t i = 0; i < ctrl->propCount; i++) { addPropRow(ctrl->props[i].name, ctrl->props[i].value); @@ -1521,6 +1527,8 @@ void prpRefresh(DsgnStateT *ds) { snprintf(buf, sizeof(buf), "%d", (int)ds->form->height); addPropRow("Height", buf); + + addPropRow("HelpTopic", ds->form->helpTopic); } wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows); diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index 15c6097..58e3dd6 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -9,10 +9,13 @@ #include "../compiler/opcodes.h" #include +#include +#include #include #include #include #include +#include #include #include @@ -29,7 +32,9 @@ static BasCallFrameT *currentFrame(BasVmT *vm); static void defaultPrint(void *ctx, const char *text, bool newline); static BasVmResultE execArith(BasVmT *vm, uint8_t op); static BasVmResultE execCompare(BasVmT *vm, uint8_t op); +static void dirClose(void); static BasVmResultE execFileOp(BasVmT *vm, uint8_t op); +static BasVmResultE execFsOp(BasVmT *vm, uint8_t op); static BasVmResultE execLogical(BasVmT *vm, uint8_t op); static BasVmResultE execMath(BasVmT *vm, uint8_t op); static BasVmResultE execPrint(BasVmT *vm); @@ -318,6 +323,9 @@ void basVmDestroy(BasVmT *vm) { } } + // Close Dir$ iterator + dirClose(); + basStringSystemShutdown(); free(vm); } @@ -1868,6 +1876,21 @@ BasVmResultE basVmStep(BasVmT *vm) { case OP_FILE_INPUT_N: return execFileOp(vm, op); + case OP_FS_KILL: + case OP_FS_NAME: + case OP_FS_FILECOPY: + case OP_FS_MKDIR: + case OP_FS_RMDIR: + case OP_FS_CHDIR: + case OP_FS_CHDRIVE: + case OP_FS_CURDIR: + case OP_FS_DIR: + case OP_FS_DIR_NEXT: + case OP_FS_FILELEN: + case OP_FS_GETATTR: + case OP_FS_SETATTR: + return execFsOp(vm, op); + // ============================================================ // DoEvents // ============================================================ @@ -4443,6 +4466,426 @@ static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) { } +// ============================================================ +// execFsOp -- filesystem operations (Kill, Name, MkDir, etc.) +// ============================================================ + +// Dir$ state -- persists across Dir$() calls within a VM session +static DIR *sDirHandle = NULL; +static char sDirPattern[260]; +static char sDirPath[260]; + +static void dirClose(void) { + if (sDirHandle) { + closedir(sDirHandle); + sDirHandle = NULL; + } +} + + +static const char *dirNext(void) { + if (!sDirHandle) { + return NULL; + } + + struct dirent *ent; + + while ((ent = readdir(sDirHandle)) != NULL) { + if (fnmatch(sDirPattern, ent->d_name, FNM_CASEFOLD) == 0) { + return ent->d_name; + } + } + + dirClose(); + return NULL; +} + + +static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) { + switch (op) { + case OP_FS_KILL: { + BasValueT fnVal; + + if (!pop(vm, &fnVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT fnStr = basValToString(fnVal); + basValRelease(&fnVal); + + if (remove(fnStr.strVal->data) != 0) { + basValRelease(&fnStr); + runtimeError(vm, 53, "File not found"); + return BAS_VM_FILE_ERROR; + } + + basValRelease(&fnStr); + break; + } + + case OP_FS_NAME: { + BasValueT newVal; + BasValueT oldVal; + + if (!pop(vm, &newVal) || !pop(vm, &oldVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT newStr = basValToString(newVal); + BasValueT oldStr = basValToString(oldVal); + basValRelease(&newVal); + basValRelease(&oldVal); + + if (rename(oldStr.strVal->data, newStr.strVal->data) != 0) { + basValRelease(&oldStr); + basValRelease(&newStr); + runtimeError(vm, 58, "File already exists or rename failed"); + return BAS_VM_FILE_ERROR; + } + + basValRelease(&oldStr); + basValRelease(&newStr); + break; + } + + case OP_FS_FILECOPY: { + BasValueT dstVal; + BasValueT srcVal; + + if (!pop(vm, &dstVal) || !pop(vm, &srcVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT dstStr = basValToString(dstVal); + BasValueT srcStr = basValToString(srcVal); + basValRelease(&dstVal); + basValRelease(&srcVal); + + FILE *fin = fopen(srcStr.strVal->data, "rb"); + FILE *fout = NULL; + + if (fin) { + fout = fopen(dstStr.strVal->data, "wb"); + } + + basValRelease(&srcStr); + basValRelease(&dstStr); + + if (!fin || !fout) { + if (fin) { + fclose(fin); + } + + if (fout) { + fclose(fout); + } + + runtimeError(vm, 53, "File not found or cannot create destination"); + return BAS_VM_FILE_ERROR; + } + + char buf[4096]; + size_t n; + + while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) { + fwrite(buf, 1, n, fout); + } + + fclose(fin); + fclose(fout); + break; + } + + case OP_FS_MKDIR: { + BasValueT pathVal; + + if (!pop(vm, &pathVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT pathStr = basValToString(pathVal); + basValRelease(&pathVal); + + #ifdef _WIN32 + int rc = mkdir(pathStr.strVal->data); + #else + int rc = mkdir(pathStr.strVal->data, 0755); + #endif + + if (rc != 0) { + basValRelease(&pathStr); + runtimeError(vm, 75, "Path/File access error"); + return BAS_VM_FILE_ERROR; + } + + basValRelease(&pathStr); + break; + } + + case OP_FS_RMDIR: { + BasValueT pathVal; + + if (!pop(vm, &pathVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT pathStr = basValToString(pathVal); + basValRelease(&pathVal); + + if (rmdir(pathStr.strVal->data) != 0) { + basValRelease(&pathStr); + runtimeError(vm, 75, "Path/File access error"); + return BAS_VM_FILE_ERROR; + } + + basValRelease(&pathStr); + break; + } + + case OP_FS_CHDIR: { + BasValueT pathVal; + + if (!pop(vm, &pathVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT pathStr = basValToString(pathVal); + basValRelease(&pathVal); + + if (chdir(pathStr.strVal->data) != 0) { + basValRelease(&pathStr); + runtimeError(vm, 76, "Path not found"); + return BAS_VM_FILE_ERROR; + } + + basValRelease(&pathStr); + break; + } + + case OP_FS_CHDRIVE: { + BasValueT driveVal; + + if (!pop(vm, &driveVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT driveStr = basValToString(driveVal); + basValRelease(&driveVal); + + // On DOS, change to the drive's root directory + if (driveStr.strVal->data[0]) { + char drivePath[4]; + drivePath[0] = driveStr.strVal->data[0]; + drivePath[1] = ':'; + drivePath[2] = '\0'; + chdir(drivePath); + } + + basValRelease(&driveStr); + break; + } + + case OP_FS_CURDIR: { + char cwd[260]; + + if (!getcwd(cwd, sizeof(cwd))) { + cwd[0] = '\0'; + } + + BasStringT *s = basStringNew(cwd, (int32_t)strlen(cwd)); + BasValueT result; + result.type = BAS_TYPE_STRING; + result.strVal = s; + + if (!push(vm, result)) { + basStringUnref(s); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FS_DIR: { + BasValueT patVal; + + if (!pop(vm, &patVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT patStr = basValToString(patVal); + basValRelease(&patVal); + + dirClose(); + + // Split pattern into directory + filename pattern + const char *pat = patStr.strVal->data; + const char *lastSep = NULL; + + for (const char *c = pat; *c; c++) { + if (*c == '/' || *c == '\\') { + lastSep = c; + } + } + + if (lastSep) { + int32_t dirLen = (int32_t)(lastSep - pat); + + if (dirLen >= (int32_t)sizeof(sDirPath)) { + dirLen = (int32_t)sizeof(sDirPath) - 1; + } + + memcpy(sDirPath, pat, dirLen); + sDirPath[dirLen] = '\0'; + snprintf(sDirPattern, sizeof(sDirPattern), "%s", lastSep + 1); + } else { + snprintf(sDirPath, sizeof(sDirPath), "."); + snprintf(sDirPattern, sizeof(sDirPattern), "%s", pat); + } + + basValRelease(&patStr); + + if (sDirPattern[0] == '\0') { + snprintf(sDirPattern, sizeof(sDirPattern), "*"); + } + + sDirHandle = opendir(sDirPath); + + const char *match = dirNext(); + const char *text = match ? match : ""; + BasStringT *s = basStringNew(text, (int32_t)strlen(text)); + BasValueT result; + result.type = BAS_TYPE_STRING; + result.strVal = s; + + if (!push(vm, result)) { + basStringUnref(s); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FS_DIR_NEXT: { + const char *match = dirNext(); + const char *text = match ? match : ""; + BasStringT *s = basStringNew(text, (int32_t)strlen(text)); + BasValueT result; + result.type = BAS_TYPE_STRING; + result.strVal = s; + + if (!push(vm, result)) { + basStringUnref(s); + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FS_FILELEN: { + BasValueT fnVal; + + if (!pop(vm, &fnVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT fnStr = basValToString(fnVal); + basValRelease(&fnVal); + + struct stat st; + int32_t size = 0; + + if (stat(fnStr.strVal->data, &st) == 0) { + size = (int32_t)st.st_size; + } + + basValRelease(&fnStr); + + BasValueT result; + result.type = BAS_TYPE_LONG; + result.longVal = size; + + if (!push(vm, result)) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FS_GETATTR: { + BasValueT fnVal; + + if (!pop(vm, &fnVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT fnStr = basValToString(fnVal); + basValRelease(&fnVal); + + struct stat st; + int32_t attrs = 0; + + if (stat(fnStr.strVal->data, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + attrs |= 16; // vbDirectory + } + + if (!(st.st_mode & S_IWUSR)) { + attrs |= 1; // vbReadOnly + } + } + + basValRelease(&fnStr); + + BasValueT result; + result.type = BAS_TYPE_INTEGER; + result.intVal = (int16_t)attrs; + + if (!push(vm, result)) { + return BAS_VM_STACK_OVERFLOW; + } + + break; + } + + case OP_FS_SETATTR: { + BasValueT attrVal; + BasValueT fnVal; + + if (!pop(vm, &attrVal) || !pop(vm, &fnVal)) { + return BAS_VM_STACK_UNDERFLOW; + } + + BasValueT fnStr = basValToString(fnVal); + int32_t attrs = (int32_t)basValToNumber(attrVal); + basValRelease(&fnVal); + basValRelease(&attrVal); + + struct stat st; + + if (stat(fnStr.strVal->data, &st) == 0) { + mode_t mode = st.st_mode; + + if (attrs & 1) { + mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH); + } else { + mode |= S_IWUSR; + } + + chmod(fnStr.strVal->data, mode); + } + + basValRelease(&fnStr); + break; + } + + default: + return BAS_VM_BAD_OPCODE; + } + + return BAS_VM_OK; +} + + // ============================================================ // execLogical // ============================================================ diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 4819f38..8f43308 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -411,6 +411,9 @@ static void scanAppsDir(void) { // object that the shell can dlopen(). We skip progman.app to avoid listing // ourselves in the launcher grid. static void scanAppsDirRecurse(const char *dirPath) { + // Collect all entries first, close the handle, then process. + // DOS has limited file handles; keeping a DIR open while + // opening .app files for resource loading causes failures. DIR *dir = opendir(dirPath); if (!dir) { @@ -421,44 +424,37 @@ static void scanAppsDirRecurse(const char *dirPath) { return; } + char **names = NULL; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { - // Skip . and .. - if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) { continue; } - // Copy d_name before recursion — readdir may use a shared buffer - char name[MAX_PATH_LEN]; - snprintf(name, sizeof(name), "%s", ent->d_name); + arrput(names, strdup(ent->d_name)); + } + closedir(dir); + + int32_t nEntries = (int32_t)arrlen(names); + + for (int32_t i = 0; i < nEntries; i++) { char fullPath[MAX_PATH_LEN]; - snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name); + snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]); - // Check if this is a directory -- recurse into it struct stat st; if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) { scanAppsDirRecurse(fullPath); + free(names[i]); continue; } - int32_t len = strlen(name); + int32_t len = (int32_t)strlen(names[i]); - if (len < 5) { - continue; - } - - // Check for .app extension (case-insensitive) - const char *ext = name + len - 4; - - if (strcasecmp(ext, ".app") != 0) { - continue; - } - - // Skip ourselves - if (strcasecmp(name, "progman.app") == 0) { + if (len < 5 || strcasecmp(names[i] + len - 4, ".app") != 0 || strcasecmp(names[i], "progman.app") == 0) { + free(names[i]); continue; } @@ -473,7 +469,7 @@ static void scanAppsDirRecurse(const char *dirPath) { nameLen = SHELL_APP_NAME_MAX - 1; } - memcpy(newEntry.name, name, nameLen); + memcpy(newEntry.name, names[i], nameLen); newEntry.name[nameLen] = '\0'; if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') { @@ -484,20 +480,21 @@ static void scanAppsDirRecurse(const char *dirPath) { newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch); if (!newEntry.iconData) { - dvxLog("Progman: no icon32 resource in %s", name); + dvxLog("Progman: no icon32 resource in %s", names[i]); } dvxResLoadText(fullPath, "name", newEntry.name, SHELL_APP_NAME_MAX); dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip)); - dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, name, newEntry.iconData ? "yes" : "no"); + dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, names[i], newEntry.iconData ? "yes" : "no"); arrput(sAppFiles, newEntry); sAppCount = (int32_t)arrlen(sAppFiles); + free(names[i]); dvxUpdate(sAc); } - closedir(dir); + arrfree(names); } @@ -592,3 +589,8 @@ int32_t appMain(DxeAppContextT *ctx) { return 0; } + + +void appShutdown(void) { + shellUnregisterDesktopUpdate(desktopUpdate); +} diff --git a/core/Makefile b/core/Makefile index ec36dac..d6c1b21 100644 --- a/core/Makefile +++ b/core/Makefile @@ -22,15 +22,6 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) TARGETDIR = $(LIBSDIR)/kpunch/libdvx TARGET = $(TARGETDIR)/libdvx.lib -# libdvx.lib export prefixes -DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _unpack -E _text \ - -E _setClip -E _resetClip -E _stbi_ -E _stbi_write -E _dirtyList \ - -E _widget \ - -E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \ - -E _sOpen -E _sPressed -E _sDrag -E _sDrawing -E _sResize \ - -E _sListView -E _sSplitter -E _sTreeView \ - -E _accelParse -E _clipboard -E _multiClick - .PHONY: all clean all: $(TARGET) $(TARGETDIR)/libdvx.dep @@ -39,7 +30,7 @@ $(TARGETDIR)/libdvx.dep: ../config/libdvx.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/libdvx.dxe $(DVX_EXPORTS) -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/libdvx.dxe -U $(OBJS) mv $(TARGETDIR)/libdvx.dxe $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) diff --git a/core/arch.dhs b/core/arch.dhs index a7f23e5..64251e5 100644 --- a/core/arch.dhs +++ b/core/arch.dhs @@ -706,8 +706,8 @@ Each DXE module is compiled to an object file with GCC, then linked with dxe3gen # Compile i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c - # Link as DXE with exported symbols - dxe3gen -o widget.wgt -E _wgtRegister -U widget.o + # Link as DXE (exports all non-static symbols) + dxe3gen -o widget.wgt -U widget.o # Optionally append resources dvxres build widget.wgt widget.res diff --git a/core/dvxApp.c b/core/dvxApp.c index 14a1da4..f2e05af 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -144,7 +144,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win); static void pollKeyboard(AppContextT *ctx); static void pollMouse(AppContextT *ctx); static void pollWidgets(AppContextT *ctx); -static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win); static void refreshMinimizedIcons(AppContextT *ctx); static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h); static void updateCursorShape(AppContextT *ctx); @@ -950,16 +949,26 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target); if (next) { + WidgetT *prev = sFocusedWidget; sFocusedWidget = next; - wclsOnAccelActivate(next, win->widgetRoot); + if (prev) { + wgtInvalidatePaint(prev); + } - wgtInvalidate(win->widgetRoot); + wclsOnAccelActivate(next, win->widgetRoot); + wgtInvalidatePaint(next); } } else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) { + WidgetT *prev = sFocusedWidget; sFocusedWidget = target; + + if (prev && prev != target) { + wgtInvalidatePaint(prev); + } + wclsOnAccelActivate(target, win->widgetRoot); - wgtInvalidate(win->widgetRoot); + wgtInvalidatePaint(target); } return true; @@ -2880,7 +2889,7 @@ static void pollKeyboard(AppContextT *ctx) { wclsClosePopup(closing); - wgtInvalidate(closing); + wgtInvalidatePaint(closing); continue; } @@ -2951,8 +2960,26 @@ static void pollKeyboard(AppContextT *ctx) { if (next) { sOpenPopup = NULL; + WidgetT *prev = sFocusedWidget; + + // Switch focus BEFORE invalidating so paint sees + // the correct focused state for both widgets. sFocusedWidget = next; + if (prev) { + wgtInvalidatePaint(prev); + + if (prev->onBlur) { + prev->onBlur(prev); + } + } + + wgtInvalidatePaint(next); + + if (next->onFocus) { + next->onFocus(next); + } + // Scroll the widget into view if needed int32_t scrollX = win->hScroll ? win->hScroll->value : 0; int32_t scrollY = win->vScroll ? win->vScroll->value : 0; @@ -2987,8 +3014,6 @@ static void pollKeyboard(AppContextT *ctx) { break; } } - - wgtInvalidate(win->widgetRoot); } arrfree(fstack); @@ -3056,48 +3081,40 @@ static void pollMouse(AppContextT *ctx) { // rect generation for efficient repainting. static void pollWidgets(AppContextT *ctx) { - for (int32_t i = 0; i < ctx->stack.count; i++) { - WindowT *win = ctx->stack.windows[i]; + for (int32_t i = 0; i < sPollWidgetCount; i++) { + WidgetT *w = sPollWidgets[i]; + WindowT *win = w->window; - if (win->widgetRoot) { - pollWidgetsWalk(ctx, win->widgetRoot, win); + if (!win) { + continue; } - } -} - -// ============================================================ -// pollWidgetsWalk -- recursive helper -// ============================================================ - -static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) { - if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL) && wclsHas(w, WGT_METHOD_POLL)) { wclsPoll(w, win); - // If the poll dirtied internal state and the widget supports - // quickRepaint, render the dirty rows directly into the window's - // content buffer and add the affected area to the global dirty list. - if (wclsHas(w, WGT_METHOD_QUICK_REPAINT)) { - int32_t dirtyY = 0; - int32_t dirtyH = 0; + if (!wclsHas(w, WGT_METHOD_QUICK_REPAINT)) { + continue; + } - if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) { + int32_t dirtyY = 0; + int32_t dirtyH = 0; + + if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) { + win->contentDirty = true; + + if (!win->minimized) { int32_t scrollY = win->vScroll ? win->vScroll->value : 0; int32_t rectX = win->x + win->contentX; int32_t rectY = win->y + win->contentY + dirtyY - scrollY; int32_t rectW = win->contentW; - win->contentDirty = true; dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); } } } - - for (WidgetT *child = w->firstChild; child; child = child->nextSibling) { - pollWidgetsWalk(ctx, child, win); - } } + + // ============================================================ // refreshMinimizedIcons // ============================================================ @@ -3219,6 +3236,12 @@ static void updateCursorShape(AppContextT *ctx) { int32_t mx = ctx->mouseX; int32_t my = ctx->mouseY; + // Popup menus override all cursor logic -- always arrow + if (ctx->popup.active || ctx->sysMenu.active) { + ctx->cursorId = CURSOR_ARROW; + return; + } + // During active resize, keep the resize cursor if (ctx->stack.resizeWindow >= 0) { int32_t edge = ctx->stack.resizeEdge; @@ -5063,14 +5086,23 @@ bool dvxUpdate(AppContextT *ctx) { refreshMinimizedIcons(ctx); } - // Auto-paint windows that haven't had their first paint yet. - // This fires one frame after creation, giving the app time to - // add all widgets before the first onPaint. + // Flush deferred widget paints. wgtInvalidatePaint sets + // widgetPaintPending instead of calling dvxInvalidateWindow inline, + // so multiple invalidations per frame are batched into one tree walk. + // Also handles first-paint (fullRepaint) for newly created windows. for (int32_t i = 0; i < ctx->stack.count; i++) { WindowT *win = ctx->stack.windows[i]; - if (win->fullRepaint && win->onPaint) { - dvxInvalidateWindow(ctx, win); + if ((win->widgetPaintPending || win->fullRepaint) && win->onPaint) { + win->widgetPaintPending = false; + RectT fullRect = {0, 0, win->contentW, win->contentH}; + WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); + // fullRepaint is cleared by widgetOnPaint for widget windows. + // For raw-paint windows, clear it here so they don't repaint + // every frame forever. + win->fullRepaint = false; + win->contentDirty = true; + dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); } } @@ -5095,7 +5127,7 @@ bool dvxUpdate(AppContextT *ctx) { sKeyPressedBtn->y + sKeyPressedBtn->h / 2); } - wgtInvalidate(sKeyPressedBtn); + wgtInvalidatePaint(sKeyPressedBtn); sKeyPressedBtn = NULL; } diff --git a/core/dvxTypes.h b/core/dvxTypes.h index 618c3fb..742c927 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -506,8 +506,9 @@ typedef struct WindowT { bool maximized; bool resizable; bool modal; - bool contentDirty; // true when contentBuf has changed since last icon refresh - bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones + bool contentDirty; // true when contentBuf has changed since last icon refresh + bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones + bool widgetPaintPending; // deferred widget paint (set by wgtInvalidatePaint) int32_t maxW; // maximum width (WM_MAX_FROM_SCREEN = use screen width) int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height) // Pre-maximize geometry is saved so wmRestore() can put the window diff --git a/core/dvxWgt.h b/core/dvxWgt.h index d26736a..39df81a 100644 --- a/core/dvxWgt.h +++ b/core/dvxWgt.h @@ -249,6 +249,7 @@ typedef struct WidgetT { bool readOnly; bool swallowTab; // Tab key goes to widget, not focus nav bool paintDirty; // needs repaint (set by wgtInvalidatePaint) + bool childDirty; // a descendant needs repaint (for WCLASS_PAINTS_CHILDREN) char accelKey; // lowercase accelerator character, 0 if none // Content offset: mouse event coordinates are adjusted by this amount diff --git a/core/dvxWgtP.h b/core/dvxWgtP.h index 35b84d0..11a7599 100644 --- a/core/dvxWgtP.h +++ b/core/dvxWgtP.h @@ -104,6 +104,8 @@ extern WidgetT *sFocusedWidget; extern WidgetT *sKeyPressedBtn; extern WidgetT *sOpenPopup; extern WidgetT *sDragWidget; // widget being dragged (any drag type) +extern int32_t sPollWidgetCount; +extern WidgetT **sPollWidgets; // stb_ds dynamic array extern void (*sCursorBlinkFn)(void); // ============================================================ @@ -172,6 +174,8 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font); // ============================================================ void widgetManageScrollbars(WindowT *win, AppContextT *ctx); +void widgetOnBlur(WindowT *win); +void widgetOnFocus(WindowT *win); void widgetOnKey(WindowT *win, int32_t key, int32_t mod); void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod); void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); diff --git a/core/dvxWm.c b/core/dvxWm.c index 025e264..c4f7137 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -2845,6 +2845,60 @@ MenuT *wmCreateMenu(void) { // Unlike freeMenuRecursive (which only frees submenu children because the // top-level struct is embedded), this also frees the root MenuT itself. +// ============================================================ +// wmDrawVScrollbarAt +// ============================================================ + +void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems) { + int32_t sbW = SCROLLBAR_WIDTH; + + // Trough + rectFill(d, ops, x, y, sbW, h, colors->scrollbarTrough); + + BevelStyleT troughBevel; + troughBevel.highlight = colors->windowShadow; + troughBevel.shadow = colors->windowHighlight; + troughBevel.face = 0; + troughBevel.width = 1; + drawBevel(d, ops, x, y, sbW, h, &troughBevel); + + BevelStyleT btnBevel; + btnBevel.highlight = colors->windowHighlight; + btnBevel.shadow = colors->windowShadow; + btnBevel.face = colors->scrollbarBg; + btnBevel.width = 1; + + // Up arrow + drawBevel(d, ops, x, y, sbW, sbW, &btnBevel); + drawScrollbarArrow(d, ops, colors, x, y, sbW, 0); + + // Down arrow + int32_t downY = y + h - sbW; + drawBevel(d, ops, x, downY, sbW, sbW, &btnBevel); + drawScrollbarArrow(d, ops, colors, x, downY, sbW, 1); + + // Thumb + int32_t trackLen = h - sbW * 2; + + if (trackLen > 0 && totalItems > visibleItems) { + int32_t maxScroll = totalItems - visibleItems; + int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / totalItems); + + if (thumbSize < sbW) { + thumbSize = sbW; + } + + int32_t thumbPos = 0; + + if (maxScroll > 0) { + thumbPos = (int32_t)(((int64_t)scrollPos * (trackLen - thumbSize)) / maxScroll); + } + + drawBevel(d, ops, x, y + sbW + thumbPos, sbW, thumbSize, &btnBevel); + } +} + + void wmFreeMenu(MenuT *menu) { if (!menu) { return; diff --git a/core/dvxWm.h b/core/dvxWm.h index d064448..4dd1a1d 100644 --- a/core/dvxWm.h +++ b/core/dvxWm.h @@ -227,4 +227,10 @@ MenuT *wmCreateMenu(void); // heap-allocated submenu children recursively. void wmFreeMenu(MenuT *menu); +// Draw a standalone vertical scrollbar at the given screen coordinates. +// Used by popup lists (dropdown, combobox) that need a scrollbar without +// a full ScrollbarT struct. scrollPos/visibleItems/totalItems define the +// scroll state; the function computes thumb position/size internally. +void wmDrawVScrollbarAt(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t h, int32_t scrollPos, int32_t visibleItems, int32_t totalItems); + #endif // DVX_WM_H diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index f5b6e8c..9536b3a 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -2551,6 +2552,8 @@ DXE_EXPORT_TABLE(sDxeExportTable) // --- filesystem --- DXE_EXPORT(access) DXE_EXPORT(chdir) + DXE_EXPORT(chmod) + DXE_EXPORT(fnmatch) DXE_EXPORT(getcwd) DXE_EXPORT(realpath) DXE_EXPORT(stat) diff --git a/core/widgetCore.c b/core/widgetCore.c index 51c0c06..b120024 100644 --- a/core/widgetCore.c +++ b/core/widgetCore.c @@ -48,6 +48,8 @@ bool sDebugLayout = false; WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk) WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list WidgetT *sDragWidget = NULL; // widget being dragged (any drag type) +int32_t sPollWidgetCount = 0; +WidgetT **sPollWidgets = NULL; // stb_ds dynamic array // Shared clipboard -- process-wide, not per-widget. static char *sClipboard = NULL; @@ -196,6 +198,11 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) { w->visible = true; w->enabled = true; + if (w->wclass->flags & WCLASS_NEEDS_POLL) { + arrput(sPollWidgets, w); + sPollWidgetCount = (int32_t)arrlen(sPollWidgets); + } + if (parent) { w->window = parent->window; widgetAddChild(parent, w); diff --git a/core/widgetEvent.c b/core/widgetEvent.c index acf1320..5e3a93d 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -335,11 +335,11 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y wclsGetPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH); if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) { - // Click on popup item -- dispatch to widget's onMouse + // Click on popup area -- dispatch to widget's onMouse. + // The widget may keep the popup open (e.g. scrollbar click) + // or close it (item selection sets sOpenPopup = NULL). wclsOnMouse(sOpenPopup, root, x, y); - - sOpenPopup = NULL; - wgtInvalidate(root); + wgtInvalidatePaint(root); return; } @@ -368,14 +368,14 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y return; } - // Clear focus from the previously focused widget. This is done via - // the cached sFocusedWidget pointer rather than walking the tree to - // find the focused widget -- an O(1) operation vs O(n). + // Clear focus from the previously focused widget. Must set + // sFocusedWidget to NULL BEFORE invalidating so the inline paint + // sees the widget as unfocused and erases its highlight. WidgetT *prevFocus = sFocusedWidget; if (sFocusedWidget) { - wgtInvalidatePaint(sFocusedWidget); sFocusedWidget = NULL; + wgtInvalidatePaint(prevFocus); } // Dispatch to the hit widget's mouse handler via vtable. The handler @@ -465,8 +465,40 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) { sFocusedWidget->onFocus(sFocusedWidget); } +} - wgtInvalidate(root); + +// ============================================================ +// widgetOnBlur -- window lost focus +// ============================================================ +// +// When a widget-managed window loses WM focus (user clicked another +// window's title bar, or the window was minimized), clear the widget +// focus so the cursor/highlight is removed. Without this, the text +// cursor stays visible in the unfocused window. + +void widgetOnBlur(WindowT *win) { + if (sFocusedWidget && sFocusedWidget->window == win) { + WidgetT *prev = sFocusedWidget; + sFocusedWidget = NULL; + wgtInvalidatePaint(prev); + + if (prev->onBlur) { + prev->onBlur(prev); + } + } +} + + +// ============================================================ +// widgetOnFocus -- window gained focus +// ============================================================ +// +// Mark the window's content dirty so the compositor knows to +// refresh its minimized icon thumbnail if needed. + +void widgetOnFocus(WindowT *win) { + win->contentDirty = true; } diff --git a/core/widgetOps.c b/core/widgetOps.c index 0841e82..db3d771 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -13,6 +13,7 @@ #include "dvxWgtP.h" #include "dvxPlat.h" +#include "stb_ds_wrap.h" #include "../widgets/box/box.h" static bool sFullRepaint = false; @@ -81,14 +82,38 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo // The window's fullRepaint flag is stored in sFullRepaint for the // duration of the paint walk. bool dirty = w->paintDirty || sFullRepaint; - w->paintDirty = false; - if (dirty) { + // For WCLASS_PAINTS_CHILDREN widgets (TabControl, TreeView, ScrollPane, + // Splitter): the generic child recursion below can't reach their + // children, so these widgets must handle child painting themselves. + // Only call their paint when something actually needs drawing. + bool paintsChildren = w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN); + + if (paintsChildren) { + // On full repaint, ensure paintDirty is set so the paint function + // redraws its chrome (the window background was cleared). + if (sFullRepaint) { + w->paintDirty = true; + } + + // Skip entirely if nothing needs painting in this subtree + if (!w->paintDirty && !w->childDirty) { + return; + } + + // When this widget itself is dirty (will clear its background), + // all descendants must repaint on the fresh background. + bool savedFull = sFullRepaint; + + if (w->paintDirty) { + sFullRepaint = true; + } + wclsPaint(w, d, ops, font, colors); - } + sFullRepaint = savedFull; + w->paintDirty = false; + w->childDirty = false; - // Widgets that paint their own children return early - if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) { if (sDebugLayout && dirty) { debugContainerBorder(w, d, ops); } @@ -96,6 +121,12 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo return; } + w->paintDirty = false; + + if (dirty) { + wclsPaint(w, d, ops, font, colors); + } + // Always recurse into children — a clean parent may have dirty children for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { widgetPaintOne(c, d, ops, font, colors); @@ -192,6 +223,16 @@ void wgtDestroy(WidgetT *w) { sDragWidget = NULL; } + if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) { + for (int32_t i = 0; i < sPollWidgetCount; i++) { + if (sPollWidgets[i] == w) { + arrdel(sPollWidgets, i); + sPollWidgetCount = (int32_t)arrlen(sPollWidgets); + break; + } + } + } + // If this is the root, clear the window's reference if (w->window && w->window->widgetRoot == w) { w->window->widgetRoot = NULL; @@ -318,6 +359,8 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { win->onKey = widgetOnKey; win->onKeyUp = widgetOnKeyUp; win->onResize = widgetOnResize; + win->onBlur = widgetOnBlur; + win->onFocus = widgetOnFocus; return root; } @@ -386,20 +429,22 @@ void wgtInvalidatePaint(WidgetT *w) { // Mark only this widget as needing repaint w->paintDirty = true; + // Propagate childDirty up through WCLASS_PAINTS_CHILDREN ancestors + // so they know to recurse into children during partial repaints. WidgetT *root = w; while (root->parent) { root = root->parent; + + if (root->wclass && (root->wclass->flags & WCLASS_PAINTS_CHILDREN)) { + root->childDirty = true; + } } - AppContextT *ctx = (AppContextT *)root->userData; - - if (!ctx) { - return; - } - - // Partial repaint — only dirty widgets will be repainted - dvxInvalidateWindow(ctx, w->window); + // Defer the actual paint — it will happen once in the main loop + // before compositing, batching multiple invalidations into one + // tree walk instead of one per call. + w->window->widgetPaintPending = true; } diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index b0c7813..7e2fb47 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -1186,8 +1186,8 @@ img { max-width: 100%; }
  # Compile
   i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c
 
-  # Link as DXE with exported symbols
-  dxe3gen -o widget.wgt -E _wgtRegister -U widget.o
+  # Link as DXE (exports all non-static symbols)
+  dxe3gen -o widget.wgt -U widget.o
 
   # Optionally append resources
   dvxres build widget.wgt widget.res
diff --git a/listhelp/Makefile b/listhelp/Makefile index 65d8792..0b7bc31 100644 --- a/listhelp/Makefile +++ b/listhelp/Makefile @@ -24,7 +24,7 @@ $(TARGETDIR)/listhelp.dep: ../config/listhelp.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/listhelp.dxe -E _widgetDraw -E _widgetDropdown -E _widgetMax -E _widgetNavigate -E _widgetPaint -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/listhelp.dxe -U $(OBJS) mv $(TARGETDIR)/listhelp.dxe $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) diff --git a/listhelp/listHelp.c b/listhelp/listHelp.c index 475c3dd..2fe9cc0 100644 --- a/listhelp/listHelp.c +++ b/listhelp/listHelp.c @@ -108,6 +108,35 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t } +// ============================================================ +// widgetTypeAheadSearch +// ============================================================ + +int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx) { + if (itemCount <= 0 || !items) { + return -1; + } + + char upper = (ch >= 'a' && ch <= 'z') ? ch - 32 : ch; + + // Search forward from currentIdx+1, wrapping around + for (int32_t i = 1; i <= itemCount; i++) { + int32_t idx = (currentIdx + i) % itemCount; + char first = items[idx][0]; + + if (first >= 'a' && first <= 'z') { + first -= 32; + } + + if (first == upper) { + return idx; + } + } + + return -1; +} + + // ============================================================ // widgetPaintPopupList // ============================================================ @@ -115,19 +144,22 @@ int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t // Shared popup list painting for Dropdown and ComboBox. void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) { - // Draw popup border + bool hasScrollbar = (itemCount > DROPDOWN_MAX_VISIBLE); + int32_t visibleItems = popH / font->charHeight; + int32_t listW = hasScrollbar ? popW - POPUP_SCROLLBAR_W : popW; + + // Draw popup border (covers item area only, not scrollbar) BevelStyleT bevel; bevel.highlight = colors->windowHighlight; bevel.shadow = colors->windowShadow; bevel.face = colors->contentBg; bevel.width = 2; - drawBevel(d, ops, popX, popY, popW, popH, &bevel); + drawBevel(d, ops, popX, popY, listW, popH, &bevel); // Draw items - int32_t visibleItems = popH / font->charHeight; - int32_t textX = popX + TEXT_INPUT_PAD; - int32_t textY = popY + 2; - int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; + int32_t textX = popX + TEXT_INPUT_PAD; + int32_t textY = popY + 2; + int32_t textW = listW - TEXT_INPUT_PAD * 2 - 4; for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { int32_t idx = scrollPos + i; @@ -144,29 +176,77 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); } - // Draw scroll indicators if the list extends beyond visible area - if (itemCount > visibleItems) { - int32_t cx = popX + popW / 2; - uint32_t arrowC = colors->menuHighlightBg; + // Draw scrollbar + if (hasScrollbar) { + wmDrawVScrollbarAt(d, ops, colors, popX + listW, popY, popH, scrollPos, visibleItems, itemCount); + } +} - // Up triangle (point at top, wide at bottom) - if (scrollPos > 0) { - int32_t ty = popY + 2; - for (int32_t i = 0; i < 3; i++) { - drawHLine(d, ops, cx - i, ty + i, 1 + i * 2, arrowC); - } +// ============================================================ +// widgetPopupScrollbarClick +// ============================================================ + +bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos) { + if (itemCount <= DROPDOWN_MAX_VISIBLE) { + return false; + } + + int32_t listW = popW - POPUP_SCROLLBAR_W; + int32_t sbX = popX + listW; + + if (x < sbX || x >= sbX + POPUP_SCROLLBAR_W) { + return false; + } + + int32_t maxScroll = itemCount - visibleItems; + int32_t relY = y - popY; + + if (relY < POPUP_SCROLLBAR_W) { + // Up arrow + if (*scrollPos > 0) { + (*scrollPos)--; + } + } else if (relY >= popH - POPUP_SCROLLBAR_W) { + // Down arrow + if (*scrollPos < maxScroll) { + (*scrollPos)++; + } + } else { + // Trough — page up/down based on which side of thumb + int32_t trackLen = popH - POPUP_SCROLLBAR_W * 2; + int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / itemCount); + + if (thumbSize < POPUP_SCROLLBAR_W) { + thumbSize = POPUP_SCROLLBAR_W; } - // Down triangle (wide at top, point at bottom) - if (scrollPos + visibleItems < itemCount) { - int32_t by = popY + popH - 4; + int32_t thumbPos = 0; - for (int32_t i = 0; i < 3; i++) { - drawHLine(d, ops, cx - i, by - i, 1 + i * 2, arrowC); + if (maxScroll > 0) { + thumbPos = (int32_t)(((int64_t)(*scrollPos) * (trackLen - thumbSize)) / maxScroll); + } + + int32_t clickInTrack = relY - POPUP_SCROLLBAR_W; + + if (clickInTrack < thumbPos) { + // Page up + *scrollPos -= visibleItems; + + if (*scrollPos < 0) { + *scrollPos = 0; + } + } else if (clickInTrack >= thumbPos + thumbSize) { + // Page down + *scrollPos += visibleItems; + + if (*scrollPos > maxScroll) { + *scrollPos = maxScroll; } } } + + return true; } @@ -193,6 +273,11 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten *popW = w->w; *popH = visibleItems * font->charHeight + 4; + // Add scrollbar width when list exceeds visible area + if (itemCount > DROPDOWN_MAX_VISIBLE) { + *popW += POPUP_SCROLLBAR_W; + } + if (w->y + w->h + *popH <= contentH) { *popY = w->y + w->h; } else { diff --git a/listhelp/listHelp.h b/listhelp/listHelp.h index c19b941..c49f6c7 100644 --- a/listhelp/listHelp.h +++ b/listhelp/listHelp.h @@ -11,6 +11,7 @@ #define DROPDOWN_BTN_WIDTH 16 #define DROPDOWN_MAX_VISIBLE 8 +#define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH // ============================================================ // Dropdown arrow glyph @@ -42,4 +43,21 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); +// ============================================================ +// Type-ahead search +// ============================================================ + +// Search items[] for the next entry starting with ch (case-insensitive), +// starting after currentIdx and wrapping around. Returns the matching +// index, or -1 if no match found. +int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx); + +// ============================================================ +// Popup scrollbar hit testing +// ============================================================ + +// Returns true if the click at (x, y) is on the popup scrollbar. +// Updates *scrollPos in place (clamped to valid range). +bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos); + #endif // LIST_HELP_H diff --git a/loader/loaderMain.c b/loader/loaderMain.c index efcf6d7..5a1a99a 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -227,49 +227,55 @@ static void readDeps(ModuleT *mod) { // ============================================================ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { + // Collect all entries first, close the handle, then process. + // DOS has limited file handles; keeping a DIR open during + // recursion or stat() causes intermittent failures. DIR *dir = opendir(dirPath); if (!dir) { return; } + char **names = NULL; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { - // Copy d_name — readdir may use a shared buffer across recursion - char name[DVX_MAX_PATH]; - snprintf(name, sizeof(name), "%s", ent->d_name); - - // Skip . and .. - if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { + if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) { continue; } + arrput(names, strdup(ent->d_name)); + } + + closedir(dir); + + int32_t count = (int32_t)arrlen(names); + int32_t extLen = (int32_t)strlen(ext); + + for (int32_t i = 0; i < count; i++) { char path[DVX_MAX_PATH]; - snprintf(path, sizeof(path), "%s%c%s", dirPath, DVX_PATH_SEP, name); + snprintf(path, sizeof(path), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]); - // Check for matching extension - int32_t nameLen = strlen(name); - int32_t extLen = strlen(ext); + int32_t nameLen = (int32_t)strlen(names[i]); - if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) { + if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) { ModuleT mod; memset(&mod, 0, sizeof(mod)); snprintf(mod.path, sizeof(mod.path), "%s", path); extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName)); arrput(*mods, mod); - continue; + } else { + struct stat st; + + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + scanDir(path, ext, mods); + } } - // Recurse into subdirectories - struct stat st; - - if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { - scanDir(path, ext, mods); - } + free(names[i]); } - closedir(dir); + arrfree(names); } @@ -500,13 +506,13 @@ bool platformGlobMatch(const char *pattern, const char *name) { // Recursively count .hcf files under the given directory static int32_t countHcfFilesRecurse(const char *dirPath) { - int32_t count = 0; DIR *dir = opendir(dirPath); if (!dir) { return 0; } + char **names = NULL; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { @@ -514,31 +520,36 @@ static int32_t countHcfFilesRecurse(const char *dirPath) { continue; } - // Copy d_name before any recursion — readdir may use a shared buffer - char name[DVX_MAX_PATH]; - snprintf(name, sizeof(name), "%s", ent->d_name); - - char fullPath[DVX_MAX_PATH]; - snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name); - - struct stat st; - - if (stat(fullPath, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - count += countHcfFilesRecurse(fullPath); - } else { - int32_t nameLen = (int32_t)strlen(name); - - if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) { - count++; - } - } + arrput(names, strdup(ent->d_name)); } closedir(dir); + + int32_t count = 0; + int32_t nEntries = (int32_t)arrlen(names); + + for (int32_t i = 0; i < nEntries; i++) { + char fullPath[DVX_MAX_PATH]; + snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]); + + struct stat st; + + if (stat(fullPath, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + count += countHcfFilesRecurse(fullPath); + } else { + int32_t nameLen = (int32_t)strlen(names[i]); + + if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) { + count++; + } + } + } + + free(names[i]); + } + + arrfree(names); return count; } @@ -580,6 +591,7 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude return; } + char **names = NULL; struct dirent *ent; while ((ent = readdir(d)) != NULL) { @@ -587,32 +599,35 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude continue; } - char name[DVX_MAX_PATH]; - snprintf(name, sizeof(name), "%s", ent->d_name); - - char fullPath[DVX_MAX_PATH]; - snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, name); - - struct stat st; - - if (stat(fullPath, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - char subPattern[DVX_MAX_PATH]; - snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart); - writeGlobToResp(resp, subPattern, excludePattern); - } else if (platformGlobMatch(globPart, name)) { - if (excludePattern && platformGlobMatch(excludePattern, name)) { - continue; - } - - fprintf(resp, "%s\n", fullPath); - } + arrput(names, strdup(ent->d_name)); } closedir(d); + + int32_t nEntries = (int32_t)arrlen(names); + + for (int32_t i = 0; i < nEntries; i++) { + char fullPath[DVX_MAX_PATH]; + snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, names[i]); + + struct stat st; + + if (stat(fullPath, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + char subPattern[DVX_MAX_PATH]; + snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart); + writeGlobToResp(resp, subPattern, excludePattern); + } else if (platformGlobMatch(globPart, names[i])) { + if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) { + fprintf(resp, "%s\n", fullPath); + } + } + } + + free(names[i]); + } + + arrfree(names); } @@ -728,6 +743,7 @@ static void processHcfDir(const char *dirPath) { return; } + char **names = NULL; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { @@ -735,33 +751,37 @@ static void processHcfDir(const char *dirPath) { continue; } - // Copy d_name before any recursion — readdir may use a shared buffer - char name[DVX_MAX_PATH]; - snprintf(name, sizeof(name), "%s", ent->d_name); - - char fullPath[DVX_MAX_PATH]; - snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name); - - struct stat st; - - if (stat(fullPath, &st) != 0) { - continue; - } - - if (S_ISDIR(st.st_mode)) { - processHcfDir(fullPath); - } else { - int32_t nameLen = (int32_t)strlen(name); - - if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) { - processHcf(fullPath, dirPath); - sSplashLoaded++; - splashUpdateProgress(); - } - } + arrput(names, strdup(ent->d_name)); } closedir(dir); + + int32_t nEntries = (int32_t)arrlen(names); + + for (int32_t i = 0; i < nEntries; i++) { + char fullPath[DVX_MAX_PATH]; + snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, names[i]); + + struct stat st; + + if (stat(fullPath, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + processHcfDir(fullPath); + } else { + int32_t nameLen = (int32_t)strlen(names[i]); + + if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) { + processHcf(fullPath, dirPath); + sSplashLoaded++; + splashUpdateProgress(); + } + } + } + + free(names[i]); + } + + arrfree(names); } diff --git a/run.sh b/run.sh index d112011..4fa9cde 100755 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ #!/bin/bash -#flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf +flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x-overrides.conf -SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf +#SDL_VIDEO_X11_VISUALID= ~/bin/dosbox-staging/dosbox -conf dosbox-staging-overrides.conf diff --git a/sdk/readme.txt b/sdk/readme.txt index 5adfc80..8cb65ee 100644 --- a/sdk/readme.txt +++ b/sdk/readme.txt @@ -33,7 +33,7 @@ Building an Application 1. Write your app with appDescriptor and appMain exports 2. Compile: gcc -c -o myapp.o myapp.c -Isdk/include/core ... - 3. Link: dxe3gen -o myapp.app -E _appDescriptor -E _appMain -U myapp.o + 3. Link: dxe3gen -o myapp.app -U myapp.o 4. Optionally create a .res file and build resources with dvxres See samples/hello/ for a complete example. @@ -43,7 +43,7 @@ Building a Widget 1. Write your widget with wgtRegister export 2. Compile: gcc -c -o mywgt.o mywgt.c -Isdk/include/core - 3. Link: dxe3gen -o mywgt.wgt -E _wgtRegister -U mywgt.o + 3. Link: dxe3gen -o mywgt.wgt -U mywgt.o See samples/widget/ for a complete example. diff --git a/sdk/samples/hello/makefile b/sdk/samples/hello/makefile index 1312b12..4a22fa1 100644 --- a/sdk/samples/hello/makefile +++ b/sdk/samples/hello/makefile @@ -18,7 +18,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 \ all: hello.app hello.app: hello.o hello.res icon32.bmp - $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U hello.o + $(DXE3GEN) -o $@ -U hello.o $(DVXRES) build $@ hello.res hello.o: hello.c diff --git a/sdk/samples/library/makefile b/sdk/samples/library/makefile index cdc6d3b..f7a49dd 100644 --- a/sdk/samples/library/makefile +++ b/sdk/samples/library/makefile @@ -13,7 +13,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 all: mylib.lib mylib.lib: mylib.o - $(DXE3GEN) -o mylib.dxe -E _myLibAdd -E _myLibMul -E _myLibVersion -U $< + $(DXE3GEN) -o mylib.dxe -U $< mv mylib.dxe $@ mylib.o: mylib.c mylib.h diff --git a/sdk/samples/library/mylib.c b/sdk/samples/library/mylib.c index 714a7f4..0c0add6 100644 --- a/sdk/samples/library/mylib.c +++ b/sdk/samples/library/mylib.c @@ -2,7 +2,7 @@ // // Build: // i586-pc-msdosdjgpp-gcc -O2 -Wall -c -o mylib.o mylib.c -// dxe3gen -o mylib.lib -E _myLibAdd -E _myLibMul -E _myLibVersion -U mylib.o +// dxe3gen -o mylib.lib -U mylib.o // // Deploy: copy mylib.lib to LIBS//MYLIB/ on the target. // Add a mylib.dep file if this library depends on others. diff --git a/sdk/samples/widget/makefile b/sdk/samples/widget/makefile index b92b75a..f9c5f9f 100644 --- a/sdk/samples/widget/makefile +++ b/sdk/samples/widget/makefile @@ -16,7 +16,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 \ all: mywgt.wgt mywgt.wgt: mywgt.o mywgt.res icon24.bmp - $(DXE3GEN) -o mywgt.dxe -E _wgtRegister -U mywgt.o + $(DXE3GEN) -o mywgt.dxe -U mywgt.o mv mywgt.dxe $@ $(DVXRES) build $@ mywgt.res diff --git a/sdk/samples/widget/mywgt.c b/sdk/samples/widget/mywgt.c index e32e4d8..112373c 100644 --- a/sdk/samples/widget/mywgt.c +++ b/sdk/samples/widget/mywgt.c @@ -8,7 +8,7 @@ // // Build: // i586-pc-msdosdjgpp-gcc -O2 -Wall -I../../include/core -c -o mywgt.o mywgt.c -// dxe3gen -o mywgt.wgt -E _wgtRegister -U mywgt.o +// dxe3gen -o mywgt.wgt -U mywgt.o // // Deploy: copy mywgt.wgt to WIDGETS//MYWGT/ on the target. // Optionally include MYWGT.DHS (C API docs) and MYWGT.BHS (BASIC docs). diff --git a/serial/Makefile b/serial/Makefile index 326c1c5..9df587d 100644 --- a/serial/Makefile +++ b/serial/Makefile @@ -24,9 +24,7 @@ $(TARGETDIR)/serial.dep: ../config/serial.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/serial.dxe \ - -E _rs232 -E _pkt -E _secLink -E _secDh -E _secCipher -E _secRng \ - -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/serial.dxe -U $(OBJS) mv $(TARGETDIR)/serial.dxe $@ $(OBJDIR)/rs232.o: ../rs232/rs232.c | $(OBJDIR) diff --git a/shell/Makefile b/shell/Makefile index f769c74..c3cf63f 100644 --- a/shell/Makefile +++ b/shell/Makefile @@ -29,7 +29,7 @@ $(TARGETDIR)/dvxshell.dep: ../config/dvxshell.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/dvxshell.dxe -E _shell -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/dvxshell.dxe -U $(OBJS) mv $(TARGETDIR)/dvxshell.dxe $@ $(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR) diff --git a/sql/Makefile b/sql/Makefile index ce281b0..1f9a382 100644 --- a/sql/Makefile +++ b/sql/Makefile @@ -99,9 +99,7 @@ $(OBJDIR)/sqlite_opcodes.o: $(GEN_OPCODES_C) | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/dvxsql.dxe \ - -E _dvxSql -E _sqlite3 \ - -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/dvxsql.dxe -U $(OBJS) mv $(TARGETDIR)/dvxsql.dxe $@ $(OBJDIR): diff --git a/tasks/Makefile b/tasks/Makefile index 991ba7f..b7f17ee 100644 --- a/tasks/Makefile +++ b/tasks/Makefile @@ -20,7 +20,7 @@ TARGET = $(TARGETDIR)/libtasks.lib all: $(TARGET) $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -E _ts -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -U $(OBJS) mv $(TARGETDIR)/libtasks.dxe $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) diff --git a/texthelp/Makefile b/texthelp/Makefile index fd2f7d1..84ead97 100644 --- a/texthelp/Makefile +++ b/texthelp/Makefile @@ -24,7 +24,7 @@ $(TARGETDIR)/texthelp.dep: ../config/texthelp.dep | $(TARGETDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(TARGETDIR) - $(DXE3GEN) -o $(TARGETDIR)/texthelp.dxe -E _clipboard -E _clearOther -E _isWordChar -E _multiClick -E _widgetText -E _wordStart -E _wordEnd -U $(OBJS) + $(DXE3GEN) -o $(TARGETDIR)/texthelp.dxe -U $(OBJS) mv $(TARGETDIR)/texthelp.dxe $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) diff --git a/widgets/Makefile b/widgets/Makefile index c2bd949..278db4c 100644 --- a/widgets/Makefile +++ b/widgets/Makefile @@ -51,7 +51,6 @@ WGT_MODS = $(foreach n,$(WGT_NAMES),$(WGTDIR)/$(n)/$(n).wgt) OBJS = $(foreach w,$(WIDGETS),$(OBJDIR)/$(word 3,$(subst :, ,$w)).o) # Per-widget extra DXE3GEN flags (e.g. additional -E exports for dlsym) -EXTRA_DXE_FLAGS_datactrl = -E _dataCtrl DEPFILES = textinpt combobox spinner terminal listbox dropdown listview treeview WGT_DEPS = $(foreach d,$(DEPFILES),$(WGTDIR)/$(d)/$(d).dep) @@ -75,7 +74,7 @@ $(OBJDIR)/$(word 3,$(subst :, ,$1)).o: $(word 2,$(subst :, ,$1))/$(word 3,$(subs $$(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)) - $$(DXE3GEN) -o $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe -E _wgtRegister $$(EXTRA_DXE_FLAGS_$(word 1,$(subst :, ,$1))) -U $$< + $$(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 \ cd $(word 2,$(subst :, ,$1)) && ../$(DVXRES) build ../$$@ $(word 4,$(subst :, ,$1)).res; \ diff --git a/widgets/canvas/widgetCanvas.c b/widgets/canvas/widgetCanvas.c index a49f228..9232744 100644 --- a/widgets/canvas/widgetCanvas.c +++ b/widgets/canvas/widgetCanvas.c @@ -695,23 +695,27 @@ void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma return; } - // Draw a sunken bevel border around the canvas + // Bevel wraps tightly around the bitmap, not the full widget. + // If the layout stretched the widget beyond the bitmap, fill + // the excess area with content background. + int32_t bevelW = cd->canvasW + CANVAS_BORDER * 2; + int32_t bevelH = cd->canvasH + CANVAS_BORDER * 2; + + if (w->w > bevelW || w->h > bevelH) { + rectFill(d, ops, w->x, w->y, w->w, w->h, colors->contentBg); + } + BevelStyleT sunken; sunken.highlight = colors->windowShadow; sunken.shadow = colors->windowHighlight; sunken.face = 0; sunken.width = CANVAS_BORDER; - drawBevel(d, ops, w->x, w->y, w->w, w->h, &sunken); + drawBevel(d, ops, w->x, w->y, bevelW, bevelH, &sunken); // Blit the canvas data inside the border - int32_t imgW = cd->canvasW; - int32_t imgH = cd->canvasH; - int32_t dx = w->x + CANVAS_BORDER; - int32_t dy = w->y + CANVAS_BORDER; - - rectCopy(d, ops, dx, dy, + rectCopy(d, ops, w->x + CANVAS_BORDER, w->y + CANVAS_BORDER, cd->pixelData, cd->canvasPitch, - 0, 0, imgW, imgH); + 0, 0, cd->canvasW, cd->canvasH); } diff --git a/widgets/checkbox/widgetCheckbox.c b/widgets/checkbox/widgetCheckbox.c index 6c5fcf5..3b6050e 100644 --- a/widgets/checkbox/widgetCheckbox.c +++ b/widgets/checkbox/widgetCheckbox.c @@ -129,6 +129,9 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; + // Clear full widget area so old focus rect is erased + rectFill(d, ops, w->x, w->y, w->w, w->h, bg); + // Draw checkbox box BevelStyleT bevel; bevel.highlight = colors->windowShadow; @@ -166,7 +169,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } if (w == sFocusedWidget) { - drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); + drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg); } } diff --git a/widgets/comboBox/widgetComboBox.c b/widgets/comboBox/widgetComboBox.c index aa4158e..735618c 100644 --- a/widgets/comboBox/widgetComboBox.c +++ b/widgets/comboBox/widgetComboBox.c @@ -166,6 +166,24 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { wgtInvalidatePaint(w); return; } + + // Type-ahead in open popup + if (key >= 0x20 && key < 0x7F) { + int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->hoverIdx); + + if (found >= 0) { + d->hoverIdx = found; + + if (d->hoverIdx < d->listScrollPos) { + d->listScrollPos = d->hoverIdx; + } else if (d->hoverIdx >= d->listScrollPos + DROPDOWN_MAX_VISIBLE) { + d->listScrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1; + } + + wgtInvalidatePaint(w); + return; + } + } } // Down arrow on closed combobox opens the popup @@ -211,7 +229,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { sFocusedWidget = w; ComboBoxDataT *d = (ComboBoxDataT *)w->data; - // If popup is open, this click is on a popup item -- select it + // If popup is open, this click is on a popup item or scrollbar if (d->open) { AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; @@ -222,6 +240,14 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { widgetDropdownPopupRect(w, font, w->window->contentH, d->itemCount, &popX, &popY, &popW, &popH); + int32_t visibleItems = popH / font->charHeight; + + // Check scrollbar click first + if (widgetPopupScrollbarClick(vx, vy, popX, popY, popW, popH, d->itemCount, visibleItems, &d->listScrollPos)) { + wgtInvalidatePaint(w); + return; + } + int32_t itemIdx = d->listScrollPos + (vy - popY - 2) / font->charHeight; if (itemIdx >= 0 && itemIdx < d->itemCount) { diff --git a/widgets/dropdown/widgetDropdown.c b/widgets/dropdown/widgetDropdown.c index 67e6d13..24e3173 100644 --- a/widgets/dropdown/widgetDropdown.c +++ b/widgets/dropdown/widgetDropdown.c @@ -110,7 +110,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { DropdownDataT *d = (DropdownDataT *)w->data; if (d->open) { - // Popup is open -- navigate items + // Popup is open -- navigate items if (key == (0x48 | 0x100)) { if (d->hoverIdx > 0) { d->hoverIdx--; @@ -135,6 +135,18 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { if (w->onChange) { w->onChange(w); } + } else if (key >= 0x20 && key < 0x7F) { + int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->hoverIdx); + + if (found >= 0) { + d->hoverIdx = found; + + if (d->hoverIdx < d->scrollPos) { + d->scrollPos = d->hoverIdx; + } else if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) { + d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1; + } + } } } else { // Popup is closed @@ -163,6 +175,17 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { if (d->selectedIdx > 0) { d->selectedIdx--; + if (w->onChange) { + w->onChange(w); + } + } + } else if (key >= 0x21 && key < 0x7F) { + // Type-ahead when closed (skip space — it opens the popup) + int32_t found = widgetTypeAheadSearch((char)key, d->items, d->itemCount, d->selectedIdx); + + if (found >= 0) { + d->selectedIdx = found; + if (w->onChange) { w->onChange(w); } @@ -190,7 +213,7 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { sFocusedWidget = w; DropdownDataT *d = (DropdownDataT *)w->data; - // If popup is open, this click is on a popup item -- select it + // If popup is open, this click is on a popup item or scrollbar if (d->open) { AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; @@ -201,6 +224,14 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { widgetDropdownPopupRect(w, font, w->window->contentH, d->itemCount, &popX, &popY, &popW, &popH); + int32_t visibleItems = popH / font->charHeight; + + // Check scrollbar click first + if (widgetPopupScrollbarClick(vx, vy, popX, popY, popW, popH, d->itemCount, visibleItems, &d->scrollPos)) { + wgtInvalidatePaint(w); + return; + } + int32_t itemIdx = d->scrollPos + (vy - popY - 2) / font->charHeight; if (itemIdx >= 0 && itemIdx < d->itemCount) { diff --git a/widgets/listBox/widgetListBox.c b/widgets/listBox/widgetListBox.c index 670e28d..b53e490 100644 --- a/widgets/listBox/widgetListBox.c +++ b/widgets/listBox/widgetListBox.c @@ -233,6 +233,11 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { int32_t newSel = widgetNavigateIndex(key, sel, d->itemCount, visibleRows); + // Type-ahead: printable character searches for next matching item + if (newSel < 0 && key >= 0x20 && key < 0x7F) { + newSel = widgetTypeAheadSearch((char)key, d->items, d->itemCount, sel); + } + if (newSel < 0) { return; } diff --git a/widgets/listView/widgetListView.c b/widgets/listView/widgetListView.c index f047756..7dc668c 100644 --- a/widgets/listView/widgetListView.c +++ b/widgets/listView/widgetListView.c @@ -483,6 +483,34 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, visibleRows); + // Type-ahead: search first column for next matching row + if (newDisplaySel < 0 && key >= 0x20 && key < 0x7F && lv->cellData && lv->colCount > 0) { + char upper = (char)key; + + if (upper >= 'a' && upper <= 'z') { + upper -= 32; + } + + for (int32_t i = 1; i <= rowCount; i++) { + int32_t dispRow = (displaySel + i) % rowCount; + int32_t dataRow = sortIdx ? sortIdx[dispRow] : dispRow; + const char *cell = lv->cellData[dataRow * lv->colCount]; + + if (cell && cell[0]) { + char first = cell[0]; + + if (first >= 'a' && first <= 'z') { + first -= 32; + } + + if (first == upper) { + newDisplaySel = dispRow; + break; + } + } + } + } + if (newDisplaySel < 0) { return; } diff --git a/widgets/radio/widgetRadio.c b/widgets/radio/widgetRadio.c index fbd2fe2..fb079cf 100644 --- a/widgets/radio/widgetRadio.c +++ b/widgets/radio/widgetRadio.c @@ -230,6 +230,9 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; + // Clear full widget area so old focus rect is erased + rectFill(d, ops, w->x, w->y, w->w, w->h, bg); + // Draw diamond-shaped radio box int32_t bx = w->x; int32_t mid = CHECKBOX_BOX_SIZE / 2; @@ -289,7 +292,7 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap } if (w == sFocusedWidget) { - drawFocusRect(d, ops, labelX - 1, labelY - 1, labelW + 2, font->charHeight + 2, fg); + drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg); } } diff --git a/widgets/scrollPane/widgetScrollPane.c b/widgets/scrollPane/widgetScrollPane.c index 19d05d2..9d006f8 100644 --- a/widgets/scrollPane/widgetScrollPane.c +++ b/widgets/scrollPane/widgetScrollPane.c @@ -791,19 +791,22 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); - // Sunken border - BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w)); - drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); + bool selfDirty = w->paintDirty; // Clip to content area and paint children int32_t oldClipX = d->clipX; int32_t oldClipY = d->clipY; int32_t oldClipW = d->clipW; int32_t oldClipH = d->clipH; - setClipRect(d, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH); - // Fill background - rectFill(d, ops, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH, bg); + if (selfDirty) { + // Full redraw: border + background + scrollbars + BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w)); + drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); + rectFill(d, ops, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH, bg); + } + + setClipRect(d, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH); // Paint children (already positioned by layout with scroll offset) for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { @@ -812,6 +815,10 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); + if (!selfDirty) { + return; + } + // Draw scrollbars if (needVSb) { int32_t sbX = w->x + w->w - spBorder(w) - SP_SB_W; diff --git a/widgets/slider/widgetSlider.c b/widgets/slider/widgetSlider.c index 5eb4399..933dcf7 100644 --- a/widgets/slider/widgetSlider.c +++ b/widgets/slider/widgetSlider.c @@ -202,9 +202,13 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma (void)font; SliderDataT *sd = (SliderDataT *)w->data; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t tickFg = w->enabled ? fg : colors->windowShadow; uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough; + // Clear full widget area so old thumb position is erased + rectFill(d, ops, w->x, w->y, w->w, w->h, bg); + int32_t range = sd->maxValue - sd->minValue; if (range <= 0) { diff --git a/widgets/tabControl/widgetTabControl.c b/widgets/tabControl/widgetTabControl.c index 0664a1c..b1905ad 100644 --- a/widgets/tabControl/widgetTabControl.c +++ b/widgets/tabControl/widgetTabControl.c @@ -352,6 +352,8 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy if (hit->onChange) { hit->onChange(hit); } + + wgtInvalidate(hit); } break; @@ -386,6 +388,11 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B TabControlDataT *td = (TabControlDataT *)w->data; int32_t tabH = font->charHeight + TAB_PAD_V * 2; bool scroll = tabNeedScroll(w, font); + bool selfDirty = w->paintDirty; + + if (!selfDirty) { + goto paintChildren; + } // Content panel BevelStyleT panelBevel; @@ -505,15 +512,17 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); +paintChildren: + ; // Paint only active tab page's children - tabIdx = 0; + int32_t activeIdx = 0; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->type != sTabPageTypeId) { continue; } - if (tabIdx == td->activeTab) { + if (activeIdx == td->activeTab) { for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) { widgetPaintOne(gc, d, ops, font, colors); } @@ -521,7 +530,7 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B break; } - tabIdx++; + activeIdx++; } } diff --git a/widgets/textInput/textInpt.h b/widgets/textInput/textInpt.h index 28633f8..b60b16d 100644 --- a/widgets/textInput/textInpt.h +++ b/widgets/textInput/textInpt.h @@ -31,6 +31,7 @@ typedef struct { int32_t (*getCursorLine)(const WidgetT *w); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); + void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count); } TextInputApiT; static inline const TextInputApiT *dvxTextInputApi(void) { @@ -56,5 +57,6 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w) #define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn) #define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz) +#define wgtTextAreaSetSyntaxColors(w, colors, count) dvxTextInputApi()->setSyntaxColors(w, colors, count) #endif // TEXTINPT_H diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index 23b7175..677f346 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -61,6 +61,16 @@ 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, @@ -137,6 +147,9 @@ typedef struct { // Gutter click callback (optional). Fired when user clicks in the gutter. void (*onGutterClick)(WidgetT *w, int32_t lineNum); + // Custom syntax colors (0x00RRGGBB; 0 = use default hardcoded color) + uint32_t customSyntaxColors[SYNTAX_MAX]; + // Pre-allocated paint buffers (avoid 3KB stack alloc per visible line per frame) uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN) char *expandBuf; // tab-expanded text (MAX_COLORIZE_LEN) @@ -160,22 +173,12 @@ typedef struct { // Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c) #define CURSOR_BLINK_MS 250 -// 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 - // ============================================================ // Prototypes // ============================================================ -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 ColorSchemeT *colors); -static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors); +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 uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom); static bool maskCharValid(char slot, char ch); static int32_t maskFirstSlot(const char *mask); static bool maskIsSlot(char ch); @@ -2046,16 +2049,19 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { // syntaxColor // ============================================================ -static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors) { - (void)colors; +static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) { + if (idx > 0 && 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); // dark blue - case SYNTAX_STRING: return packColor(d, 128, 0, 0); // dark red - case SYNTAX_COMMENT: return packColor(d, 0, 128, 0); // dark green - case SYNTAX_NUMBER: return packColor(d, 128, 0, 128); // purple - case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0); // dark yellow - case SYNTAX_TYPE: return packColor(d, 0, 128, 128); // teal + 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; } } @@ -2068,7 +2074,7 @@ static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, // 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 ColorSchemeT *colors) { +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) { @@ -2079,7 +2085,7 @@ static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFont runEnd++; } - uint32_t fg = syntaxColor(d, curColor, defaultFg, colors); + 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; } @@ -2300,7 +2306,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // 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, colors); + 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); } @@ -2314,7 +2320,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // 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, colors); + 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); } @@ -2338,7 +2344,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // 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, colors); + 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); } @@ -3121,6 +3127,26 @@ void wgtTextAreaSetGutterClickCallback(WidgetT *w, void (*fn)(WidgetT *, int32_t } +void wgtTextAreaSetSyntaxColors(WidgetT *w, const uint32_t *colors, int32_t count) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + memset(ta->customSyntaxColors, 0, sizeof(ta->customSyntaxColors)); + + if (colors && count > 0) { + if (count > SYNTAX_MAX) { + count = SYNTAX_MAX; + } + + memcpy(ta->customSyntaxColors, colors, count * sizeof(uint32_t)); + } + + wgtInvalidatePaint(w); +} + + int32_t wgtTextAreaGetCursorLine(const WidgetT *w) { if (!w || w->type != sTextAreaTypeId) { return 1; @@ -3395,6 +3421,7 @@ static const struct { int32_t (*getCursorLine)(const WidgetT *w); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); + void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count); } sApi = { .create = wgtTextInput, .password = wgtPasswordInput, @@ -3412,7 +3439,8 @@ static const struct { .setLineDecorator = wgtTextAreaSetLineDecorator, .getCursorLine = wgtTextAreaGetCursorLine, .setGutterClick = wgtTextAreaSetGutterClickCallback, - .getWordAtCursor = wgtTextAreaGetWordAtCursor + .getWordAtCursor = wgtTextAreaGetWordAtCursor, + .setSyntaxColors = wgtTextAreaSetSyntaxColors }; // Per-type APIs for the designer diff --git a/widgets/treeView/widgetTreeView.c b/widgets/treeView/widgetTreeView.c index 9fee48d..f58d805 100644 --- a/widgets/treeView/widgetTreeView.c +++ b/widgets/treeView/widgetTreeView.c @@ -891,6 +891,63 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { selTi->selected = !selTi->selected; tv->anchorItem = sel; } + } else if (key >= 0x20 && key < 0x7F) { + // Type-ahead: search visible items for next match + char upper = (char)key; + + if (upper >= 'a' && upper <= 'z') { + upper -= 32; + } + + WidgetT *start = sel ? sel : firstVisibleItem(w); + + if (start) { + WidgetT *cur = nextVisibleItem(start, w); + + if (!cur) { + cur = firstVisibleItem(w); + } + + while (cur && cur != start) { + TreeItemDataT *ti = (TreeItemDataT *)cur->data; + + if (ti->text && ti->text[0]) { + char first = ti->text[0]; + + if (first >= 'a' && first <= 'z') { + first -= 32; + } + + if (first == upper) { + setSelectedItem(w, cur); + break; + } + } + + cur = nextVisibleItem(cur, w); + + if (!cur) { + cur = firstVisibleItem(w); + } + } + + // Check the start item itself if we wrapped all the way around + if (cur == start && sel) { + TreeItemDataT *ti = (TreeItemDataT *)start->data; + + if (ti->text && ti->text[0]) { + char first = ti->text[0]; + + if (first >= 'a' && first <= 'z') { + first -= 32; + } + + if (first == upper && cur != sel) { + setSelectedItem(w, cur); + } + } + } + } } else { return; }