Bug and performance fixes. Lots of crap I forgot.

This commit is contained in:
Scott Duensing 2026-04-13 20:03:24 -05:00
parent 454a3620f7
commit 5f305dd14c
59 changed files with 1815 additions and 307 deletions

View file

@ -29,28 +29,28 @@ dvxdemo: $(BINDIR)/kpunch/dvxdemo/dvxdemo.app
dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app dvxhelp: $(BINDIR)/kpunch/dvxhelp/dvxhelp.app
$(BINDIR)/kpunch/cpanel/cpanel.app: $(OBJDIR)/cpanel.o cpanel/cpanel.res cpanel/icon32.bmp | $(BINDIR)/kpunch/cpanel $(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 $(DVXRES) build $@ cpanel/cpanel.res
$(BINDIR)/kpunch/imgview/imgview.app: $(OBJDIR)/imgview.o imgview/imgview.res imgview/icon32.bmp | $(BINDIR)/kpunch/imgview $(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 $(DVXRES) build $@ imgview/imgview.res
$(BINDIR)/kpunch/progman/progman.app: $(OBJDIR)/progman.o | $(BINDIR)/kpunch/progman $(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 $(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 $(DVXRES) build $@ notepad/notepad.res
$(BINDIR)/kpunch/clock/clock.app: $(OBJDIR)/clock.o clock/clock.res clock/icon32.bmp | $(BINDIR)/kpunch/clock $(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 $(DVXRES) build $@ clock/clock.res
DVXDEMO_BMPS = logo.bmp new.bmp open.bmp sample.bmp save.bmp 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 $(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 $(DVXRES) build $@ dvxdemo/dvxdemo.res
cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/ cp $(addprefix dvxdemo/,$(DVXDEMO_BMPS)) $(BINDIR)/kpunch/dvxdemo/
@ -73,7 +73,7 @@ $(OBJDIR)/dvxdemo.o: dvxdemo/dvxdemo.c | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(BINDIR)/kpunch/dvxhelp/dvxhelp.app: $(OBJDIR)/dvxhelp.o dvxhelp/dvxhelp.res dvxhelp/icon32.bmp | $(BINDIR)/kpunch/dvxhelp $(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 $(DVXRES) build $@ dvxhelp/dvxhelp.res
$(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR) $(OBJDIR)/dvxhelp.o: dvxhelp/dvxhelp.c dvxhelp/hlpformat.h | $(OBJDIR)

View file

@ -71,9 +71,7 @@ $(TEST_QUICK): $(TEST_QUICK_SRCS) | $(BINDIR)
# Runtime library DXE (exports symbols via dlregsym constructor) # Runtime library DXE (exports symbols via dlregsym constructor)
$(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR) $(RT_TARGET): $(RT_OBJS) | $(RT_TARGETDIR)
$(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe \ $(DXE3GEN) -o $(RT_TARGETDIR)/basrt.dxe -U $(RT_OBJS)
-E _bas -E _BAS \
-U $(RT_OBJS)
mv $(RT_TARGETDIR)/basrt.dxe $@ mv $(RT_TARGETDIR)/basrt.dxe $@
$(RT_TARGETDIR)/basrt.dep: ../../config/basrt.dep | $(RT_TARGETDIR) $(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) # IDE app DXE (compiler linked in, runtime from basrt.lib)
$(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR) $(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 $(DVXRES) build $@ dvxbasic.res

View file

@ -32,7 +32,11 @@ static const KeywordEntryT sKeywords[] = {
{ "BYVAL", TOK_BYVAL }, { "BYVAL", TOK_BYVAL },
{ "CALL", TOK_CALL }, { "CALL", TOK_CALL },
{ "CASE", TOK_CASE }, { "CASE", TOK_CASE },
{ "CHDIR", TOK_CHDIR },
{ "CHDRIVE", TOK_CHDRIVE },
{ "CLOSE", TOK_CLOSE }, { "CLOSE", TOK_CLOSE },
{ "CURDIR", TOK_CURDIR },
{ "CURDIR$", TOK_CURDIR },
{ "CONST", TOK_CONST }, { "CONST", TOK_CONST },
{ "DATA", TOK_DATA }, { "DATA", TOK_DATA },
{ "DECLARE", TOK_DECLARE }, { "DECLARE", TOK_DECLARE },
@ -43,6 +47,8 @@ static const KeywordEntryT sKeywords[] = {
{ "DEFSNG", TOK_DEFSNG }, { "DEFSNG", TOK_DEFSNG },
{ "DEFSTR", TOK_DEFSTR }, { "DEFSTR", TOK_DEFSTR },
{ "DIM", TOK_DIM }, { "DIM", TOK_DIM },
{ "DIR", TOK_DIR },
{ "DIR$", TOK_DIR },
{ "DO", TOK_DO }, { "DO", TOK_DO },
{ "DOEVENTS", TOK_DOEVENTS }, { "DOEVENTS", TOK_DOEVENTS },
{ "DOUBLE", TOK_DOUBLE }, { "DOUBLE", TOK_DOUBLE },
@ -57,9 +63,12 @@ static const KeywordEntryT sKeywords[] = {
{ "EXPLICIT", TOK_EXPLICIT }, { "EXPLICIT", TOK_EXPLICIT },
{ "EXIT", TOK_EXIT }, { "EXIT", TOK_EXIT },
{ "FALSE", TOK_FALSE_KW }, { "FALSE", TOK_FALSE_KW },
{ "FILECOPY", TOK_FILECOPY },
{ "FILELEN", TOK_FILELEN },
{ "FOR", TOK_FOR }, { "FOR", TOK_FOR },
{ "FUNCTION", TOK_FUNCTION }, { "FUNCTION", TOK_FUNCTION },
{ "GET", TOK_GET }, { "GET", TOK_GET },
{ "GETATTR", TOK_GETATTR },
{ "GOSUB", TOK_GOSUB }, { "GOSUB", TOK_GOSUB },
{ "GOTO", TOK_GOTO }, { "GOTO", TOK_GOTO },
{ "HIDE", TOK_HIDE }, { "HIDE", TOK_HIDE },
@ -70,6 +79,7 @@ static const KeywordEntryT sKeywords[] = {
{ "INPUT", TOK_INPUT }, { "INPUT", TOK_INPUT },
{ "INTEGER", TOK_INTEGER }, { "INTEGER", TOK_INTEGER },
{ "IS", TOK_IS }, { "IS", TOK_IS },
{ "KILL", TOK_KILL },
{ "LBOUND", TOK_LBOUND }, { "LBOUND", TOK_LBOUND },
{ "LET", TOK_LET }, { "LET", TOK_LET },
{ "LINE", TOK_LINE }, { "LINE", TOK_LINE },
@ -77,10 +87,12 @@ static const KeywordEntryT sKeywords[] = {
{ "LONG", TOK_LONG }, { "LONG", TOK_LONG },
{ "LOOP", TOK_LOOP }, { "LOOP", TOK_LOOP },
{ "ME", TOK_ME }, { "ME", TOK_ME },
{ "MKDIR", TOK_MKDIR },
{ "MOD", TOK_MOD }, { "MOD", TOK_MOD },
{ "INPUTBOX", TOK_INPUTBOX }, { "INPUTBOX", TOK_INPUTBOX },
{ "INPUTBOX$", TOK_INPUTBOX }, { "INPUTBOX$", TOK_INPUTBOX },
{ "MSGBOX", TOK_MSGBOX }, { "MSGBOX", TOK_MSGBOX },
{ "NAME", TOK_NAME },
{ "NEXT", TOK_NEXT }, { "NEXT", TOK_NEXT },
{ "NOT", TOK_NOT }, { "NOT", TOK_NOT },
{ "ON", TOK_ON }, { "ON", TOK_ON },
@ -99,9 +111,11 @@ static const KeywordEntryT sKeywords[] = {
{ "RESTORE", TOK_RESTORE }, { "RESTORE", TOK_RESTORE },
{ "RESUME", TOK_RESUME }, { "RESUME", TOK_RESUME },
{ "RETURN", TOK_RETURN }, { "RETURN", TOK_RETURN },
{ "RMDIR", TOK_RMDIR },
{ "SEEK", TOK_SEEK }, { "SEEK", TOK_SEEK },
{ "SELECT", TOK_SELECT }, { "SELECT", TOK_SELECT },
{ "SET", TOK_SET }, { "SET", TOK_SET },
{ "SETATTR", TOK_SETATTR },
{ "SHARED", TOK_SHARED }, { "SHARED", TOK_SHARED },
{ "SHELL", TOK_SHELL }, { "SHELL", TOK_SHELL },
{ "SHOW", TOK_SHOW }, { "SHOW", TOK_SHOW },

View file

@ -169,6 +169,20 @@ typedef enum {
TOK_WRITE, TOK_WRITE,
TOK_XOR, 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 // File modes
TOK_APPEND, TOK_APPEND,
TOK_BINARY, TOK_BINARY,

View file

@ -342,8 +342,23 @@
#define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string #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 #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 // 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 // Halt

View file

@ -141,6 +141,8 @@ static void parsePrimary(BasParserT *p);
static void parseAssignOrCall(BasParserT *p); static void parseAssignOrCall(BasParserT *p);
static void parseBeginForm(BasParserT *p); static void parseBeginForm(BasParserT *p);
static void parseChDir(BasParserT *p);
static void parseChDrive(BasParserT *p);
static void parseClose(BasParserT *p); static void parseClose(BasParserT *p);
static void parseConst(BasParserT *p); static void parseConst(BasParserT *p);
static void parseData(BasParserT *p); static void parseData(BasParserT *p);
@ -154,6 +156,7 @@ static void parseEnd(BasParserT *p);
static void parseEndForm(BasParserT *p); static void parseEndForm(BasParserT *p);
static void parseErase(BasParserT *p); static void parseErase(BasParserT *p);
static void parseExit(BasParserT *p); static void parseExit(BasParserT *p);
static void parseFileCopy(BasParserT *p);
static void parseFor(BasParserT *p); static void parseFor(BasParserT *p);
static void parseFunction(BasParserT *p); static void parseFunction(BasParserT *p);
static void parseGet(BasParserT *p); static void parseGet(BasParserT *p);
@ -161,8 +164,11 @@ static void parseGosub(BasParserT *p);
static void parseGoto(BasParserT *p); static void parseGoto(BasParserT *p);
static void parseIf(BasParserT *p); static void parseIf(BasParserT *p);
static void parseInput(BasParserT *p); static void parseInput(BasParserT *p);
static void parseKill(BasParserT *p);
static void parseLineInput(BasParserT *p); static void parseLineInput(BasParserT *p);
static void parseMkDir(BasParserT *p);
static void parseModule(BasParserT *p); static void parseModule(BasParserT *p);
static void parseName(BasParserT *p);
static void parseOn(BasParserT *p); static void parseOn(BasParserT *p);
static void parseOnError(BasParserT *p); static void parseOnError(BasParserT *p);
static void parseOpen(BasParserT *p); static void parseOpen(BasParserT *p);
@ -173,7 +179,9 @@ static void parseRead(BasParserT *p);
static void parseRedim(BasParserT *p); static void parseRedim(BasParserT *p);
static void parseRestore(BasParserT *p); static void parseRestore(BasParserT *p);
static void parseResume(BasParserT *p); static void parseResume(BasParserT *p);
static void parseRmDir(BasParserT *p);
static void parseSeek(BasParserT *p); static void parseSeek(BasParserT *p);
static void parseSetAttr(BasParserT *p);
static void parseSelectCase(BasParserT *p); static void parseSelectCase(BasParserT *p);
static void parseShell(BasParserT *p); static void parseShell(BasParserT *p);
static void parseSleep(BasParserT *p); static void parseSleep(BasParserT *p);
@ -250,6 +258,14 @@ static void addPredefConsts(BasParserT *p) {
// Show mode flags // Show mode flags
addPredefConst(p, "vbModal", 1); 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; 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]]) // InputBox$(prompt [, title [, default]])
if (tt == TOK_INPUTBOX) { if (tt == TOK_INPUTBOX) {
advance(p); 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) { static void parseShell(BasParserT *p) {
// SHELL "command" -- execute an OS command (discard return value) // SHELL "command" -- execute an OS command (discard return value)
// SHELL -- no argument, no-op in embedded context // SHELL -- no argument, no-op in embedded context
@ -4793,14 +4932,38 @@ static void parseStatement(BasParserT *p) {
parseRedim(p); parseRedim(p);
break; break;
case TOK_FILECOPY:
parseFileCopy(p);
break;
case TOK_INPUT: case TOK_INPUT:
parseInput(p); parseInput(p);
break; break;
case TOK_KILL:
parseKill(p);
break;
case TOK_MKDIR:
parseMkDir(p);
break;
case TOK_NAME:
parseName(p);
break;
case TOK_OPEN: case TOK_OPEN:
parseOpen(p); parseOpen(p);
break; break;
case TOK_CHDIR:
parseChDir(p);
break;
case TOK_CHDRIVE:
parseChDrive(p);
break;
case TOK_CLOSE: case TOK_CLOSE:
parseClose(p); parseClose(p);
break; break;
@ -4849,6 +5012,14 @@ static void parseStatement(BasParserT *p) {
parseResume(p); parseResume(p);
break; break;
case TOK_RMDIR:
parseRmDir(p);
break;
case TOK_SETATTR:
parseSetAttr(p);
break;
case TOK_RETURN: case TOK_RETURN:
advance(p); advance(p);
if (p->sym.inLocalScope) { if (p->sym.inLocalScope) {

View file

@ -624,6 +624,11 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
return basValStringFromC(text ? text : ""); return basValStringFromC(text ? text : "");
} }
// Help topic
if (strcasecmp(propName, "HelpTopic") == 0) {
return basValStringFromC(ctrl->helpTopic);
}
// Data binding properties // Data binding properties
if (strcasecmp(propName, "DataSource") == 0) { if (strcasecmp(propName, "DataSource") == 0) {
return basValStringFromC(ctrl->dataSource); return basValStringFromC(ctrl->dataSource);
@ -1071,6 +1076,16 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
continue; 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 // Layout property on a container: replace the parentStack entry
// with a layout box inside the container widget. // with a layout box inside the container widget.
// NOTE: only do this for non-default layouts (HBox, WrapBox). // 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); form->frmAutoSize = (strcasecmp(text, "True") == 0);
} else if (strcasecmp(key, "Layout") == 0) { } else if (strcasecmp(key, "Layout") == 0) {
snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text); 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; 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) // Data binding properties (stored on BasControlT, not on the widget)
if (strcasecmp(propName, "DataSource") == 0) { if (strcasecmp(propName, "DataSource") == 0) {
BasStringT *s = basValFormatString(value); BasStringT *s = basValFormatString(value);

View file

@ -54,6 +54,7 @@ typedef struct BasControlT {
char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
char dataField[BAS_MAX_CTRL_NAME]; // column name 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) int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
} BasControlT; } BasControlT;
@ -81,6 +82,7 @@ typedef struct BasFormT {
bool frmCentered; bool frmCentered;
bool frmAutoSize; bool frmAutoSize;
char frmLayout[32]; // "VBox", "HBox", or "WrapBox" 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) // Per-form variable storage (allocated at load, freed at unload)
BasValueT *formVars; BasValueT *formVars;
int32_t formVarCount; int32_t formVarCount;
@ -110,6 +112,7 @@ typedef struct {
BasFormT **forms; // stb_ds array of heap-allocated pointers BasFormT **forms; // stb_ds array of heap-allocated pointers
int32_t formCount; int32_t formCount;
BasFormT *currentForm; // form currently dispatching events BasFormT *currentForm; // form currently dispatching events
char helpFile[256]; // project help file path (for F1)
BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources
int32_t frmCacheCount; int32_t frmCacheCount;
} BasFormRtT; } BasFormRtT;

View file

@ -750,6 +750,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = 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, "Weight") == 0) { curCtrl->weight = atoi(val); }
else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); } 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 if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ }
else { setPropValue(curCtrl, key, val); } else { setPropValue(curCtrl, key, val); }
} else { } else {
@ -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, "Top") == 0) { form->top = atoi(val); }
else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); form->autoSize = false; } 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, "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); 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++) { for (int32_t j = 0; j < ctrl->propCount; j++) {
if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; } if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; }
if (strcasecmp(ctrl->props[j].name, "Text") == 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); 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 // Output menu items as nested Begin Menu blocks
{ {
int32_t menuCount = (int32_t)arrlen(ds->form->menuItems); int32_t menuCount = (int32_t)arrlen(ds->form->menuItems);

View file

@ -66,6 +66,7 @@ typedef struct {
int32_t maxWidth; // 0 = no cap (stretch to fill) int32_t maxWidth; // 0 = no cap (stretch to fill)
int32_t maxHeight; // 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) 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]; DsgnPropT props[DSGN_MAX_PROPS];
int32_t propCount; int32_t propCount;
WidgetT *widget; // live widget (created at design time for WYSIWYG) 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 centered; // true = center on screen, false = use left/top
bool autoSize; // true = dvxFitWindow, false = use width/height bool autoSize; // true = dvxFitWindow, false = use width/height
bool resizable; // true = user can resize at runtime 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 DsgnControlT **controls; // stb_ds array of heap-allocated pointers
DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus) DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus)
bool dirty; bool dirty;

View file

@ -23,6 +23,10 @@
#include "radio/radio.h" #include "radio/radio.h"
#include "textInput/textInpt.h" #include "textInput/textInpt.h"
#include "dropdown/dropdown.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 "button/button.h"
#include "splitter/splitter.h" #include "splitter/splitter.h"
#include "statusBar/statBar.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 DxeAppContextT *sCtx = NULL;
static char sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run)
static AppContextT *sAc = NULL; static AppContextT *sAc = NULL;
static PrefsHandleT *sPrefs = NULL; static PrefsHandleT *sPrefs = NULL;
static WindowT *sWin = NULL; // Main toolbar window static WindowT *sWin = NULL; // Main toolbar window
@ -794,6 +799,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Set help file and context-sensitive F1 handler // 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(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->onHelpQuery = helpQueryHandler;
ctx->helpQueryCtx = NULL; ctx->helpQueryCtx = NULL;
@ -2171,6 +2177,12 @@ static void runModule(BasModuleT *mod) {
sDbgFormRt = formRt; sDbgFormRt = formRt;
sDbgModule = mod; sDbgModule = mod;
sDbgState = DBG_RUNNING; 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(); updateProjectMenuState();
// Set breakpoints BEFORE loading forms so breakpoints in form // 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 // Run in slices of 10000 steps, yielding to DVX between slices
basVmSetStepLimit(vm, IDE_STEP_SLICE); basVmSetStepLimit(vm, IDE_STEP_SLICE);
int32_t totalSteps = 0;
BasVmResultE result; BasVmResultE result;
sStopRequested = false; sStopRequested = false;
@ -2228,7 +2239,6 @@ static void runModule(BasModuleT *mod) {
} }
result = basVmRun(vm); result = basVmRun(vm);
totalSteps += vm->stepCount;
if (result == BAS_VM_BREAKPOINT) { if (result == BAS_VM_BREAKPOINT) {
sDbgState = DBG_PAUSED; sDbgState = DBG_PAUSED;
@ -2278,8 +2288,6 @@ static void runModule(BasModuleT *mod) {
sStopRequested = false; sStopRequested = false;
while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) { while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) {
totalSteps += vm->stepCount;
vm->stepCount = 0;
if (sDbgState == DBG_PAUSED) { if (sDbgState == DBG_PAUSED) {
// Paused inside an event handler // Paused inside an event handler
@ -2321,9 +2329,7 @@ static void runModule(BasModuleT *mod) {
updateProjectMenuState(); updateProjectMenuState();
setOutputText(sOutputBuf); setOutputText(sOutputBuf);
static char statusBuf[128]; setStatus("Done.");
snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps);
setStatus(statusBuf);
// Restore IDE windows // Restore IDE windows
if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); } if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); }
@ -4691,9 +4697,34 @@ static void handleViewCmd(int32_t cmd) {
// Preferences dialog // 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 { static struct {
bool done; bool done;
bool accepted; bool accepted;
// General tab
WidgetT *renameSkipComments; WidgetT *renameSkipComments;
WidgetT *optionExplicit; WidgetT *optionExplicit;
WidgetT *tabWidthInput; WidgetT *tabWidthInput;
@ -4703,6 +4734,16 @@ static struct {
WidgetT *defVersion; WidgetT *defVersion;
WidgetT *defCopyright; WidgetT *defCopyright;
WidgetT *defDescription; 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; } 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) { static void showPreferencesDialog(void) {
memset(&sPrefsDlg, 0, sizeof(sPrefsDlg)); memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));
WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 400, 440, false); WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 420, 440, false);
if (!win) { if (!win) {
return; return;
@ -4734,8 +4862,16 @@ static void showPreferencesDialog(void) {
WidgetT *root = wgtInitWindow(sAc, win); WidgetT *root = wgtInitWindow(sAc, win);
root->spacing = wgtPixels(4); root->spacing = wgtPixels(4);
// ---- Editor section ---- // ---- Tab control ----
WidgetT *edFrame = wgtFrame(root, "Editor"); 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); edFrame->spacing = wgtPixels(2);
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming"); 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"); sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs");
wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true)); wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true));
// ---- Project Defaults section ---- // Project Defaults section
WidgetT *prjFrame = wgtFrame(root, "New Project Defaults"); WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults");
prjFrame->spacing = wgtPixels(2); prjFrame->spacing = wgtPixels(2);
prjFrame->weight = 100;
WidgetT *r1 = wgtHBox(prjFrame); WidgetT *r1 = wgtHBox(prjFrame);
r1->spacing = wgtPixels(4); r1->spacing = wgtPixels(4);
@ -4799,6 +4936,60 @@ static void showPreferencesDialog(void) {
sPrefsDlg.defDescription->minH = wgtPixels(48); sPrefsDlg.defDescription->minH = wgtPixels(48);
wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", "")); 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 ---- // ---- OK / Cancel ----
WidgetT *btnRow = wgtHBox(root); WidgetT *btnRow = wgtHBox(root);
btnRow->spacing = wgtPixels(8); btnRow->spacing = wgtPixels(8);
@ -4822,6 +5013,7 @@ static void showPreferencesDialog(void) {
} }
if (sPrefsDlg.accepted) { if (sPrefsDlg.accepted) {
// General tab
prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments)); prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit)); prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces)); prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
@ -4856,6 +5048,14 @@ static void showPreferencesDialog(void) {
val = wgtGetText(sPrefsDlg.defDescription); val = wgtGetText(sPrefsDlg.defDescription);
prefsSetString(sPrefs, "defaults", "description", val ? val : ""); 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); prefsSave(sPrefs);
} }
@ -4959,12 +5159,48 @@ static void helpQueryHandler(void *ctx) {
sCtx->helpTopic[0] = '\0'; 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 // Determine which window is focused
WindowT *focusWin = NULL; WindowT *focusWin = NULL;
if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) { if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) {
focusWin = sAc->stack.windows[sAc->stack.focusedIdx]; 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 // Code editor: look up the word under the cursor
if (focusWin == sCodeWin && sEditor) { if (focusWin == sCodeWin && sEditor) {
char word[128]; char word[128];
@ -6893,6 +7129,20 @@ static void showCodeWindow(void) {
sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE); sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
sEditor->weight = 100; sEditor->weight = 100;
wgtTextAreaSetColorize(sEditor, basicColorize, NULL); 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); wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc);
wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetGutterClick(sEditor, onGutterClick);
wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetShowLineNumbers(sEditor, true);

View file

@ -148,6 +148,9 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
val = prefsGetString(h, "Project", "Icon", NULL); val = prefsGetString(h, "Project", "Icon", NULL);
if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); } 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, ... // [Modules] section -- File0, File1, ...
for (int32_t i = 0; i < PRJ_MAX_FILES; i++) { for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
char key[16]; char key[16];
@ -208,6 +211,7 @@ bool prjSave(const PrjStateT *prj) {
if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); } if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); }
if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); } if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); }
if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); } if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); }
if (prj->helpFile[0]) { prefsSetString(h, "Project", "HelpFile", prj->helpFile); }
// [Modules] section // [Modules] section
int32_t modIdx = 0; int32_t modIdx = 0;
@ -620,6 +624,7 @@ static struct {
WidgetT *description; WidgetT *description;
WidgetT *startupForm; WidgetT *startupForm;
const char **formNames; // stb_ds array of form name strings for startup dropdown const char **formNames; // stb_ds array of form name strings for startup dropdown
WidgetT *helpFileInput;
WidgetT *iconPreview; WidgetT *iconPreview;
char iconPath[DVX_MAX_PATH]; char iconPath[DVX_MAX_PATH];
const char *appPath; 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) { static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) {
WidgetT *row = wgtHBox(parent); WidgetT *row = wgtHBox(parent);
row->spacing = wgtPixels(4); row->spacing = wgtPixels(4);
@ -927,6 +959,22 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
ppdLoadIconPreview(); 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) // Description: label above, textarea below (matches Preferences layout)
wgtLabel(root, "Description:"); wgtLabel(root, "Description:");
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC); 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); 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 // Read startup form from dropdown
if (sPpd.startupForm && sPpd.formNames) { if (sPpd.startupForm && sPpd.formNames) {
int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm); int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);

View file

@ -56,6 +56,7 @@ typedef struct {
char copyright[PRJ_MAX_STRING]; char copyright[PRJ_MAX_STRING];
char description[PRJ_MAX_DESC]; char description[PRJ_MAX_DESC];
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP 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 PrjFileT *files; // stb_ds dynamic array
int32_t fileCount; int32_t fileCount;
PrjSourceMapT *sourceMap; // stb_ds dynamic array PrjSourceMapT *sourceMap; // stb_ds dynamic array

View file

@ -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, "Visible") == 0) { return PROP_TYPE_BOOL; }
if (strcasecmp(propName, "Enabled") == 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, "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, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } 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; bool vis = ctrl->widget ? ctrl->widget->visible : true;
cascadeToChildren(sDs, ctrl->name, vis, val); cascadeToChildren(sDs, ctrl->name, vis, val);
} }
} else if (strcasecmp(propName, "HelpTopic") == 0) {
snprintf(ctrl->helpTopic, DSGN_MAX_NAME, "%s", newValue);
} else { } else {
// Try widget iface setter first // Try widget iface setter first
bool ifaceHandled = false; bool ifaceHandled = false;
@ -976,6 +979,8 @@ static void onPropDblClick(WidgetT *w) {
} else if (strcasecmp(propName, "Height") == 0) { } else if (strcasecmp(propName, "Height") == 0) {
sDs->form->height = atoi(newValue); sDs->form->height = atoi(newValue);
sDs->form->autoSize = false; sDs->form->autoSize = false;
} else if (strcasecmp(propName, "HelpTopic") == 0) {
snprintf(sDs->form->helpTopic, DSGN_MAX_NAME, "%s", newValue);
} }
sDs->form->dirty = true; sDs->form->dirty = true;
@ -1437,6 +1442,7 @@ void prpRefresh(DsgnStateT *ds) {
addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False"); addPropRow("Visible", ctrl->widget && ctrl->widget->visible ? "True" : "False");
addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False"); addPropRow("Enabled", ctrl->widget && ctrl->widget->enabled ? "True" : "False");
addPropRow("HelpTopic", ctrl->helpTopic);
for (int32_t i = 0; i < ctrl->propCount; i++) { for (int32_t i = 0; i < ctrl->propCount; i++) {
addPropRow(ctrl->props[i].name, ctrl->props[i].value); 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); snprintf(buf, sizeof(buf), "%d", (int)ds->form->height);
addPropRow("Height", buf); addPropRow("Height", buf);
addPropRow("HelpTopic", ds->form->helpTopic);
} }
wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows); wgtListViewSetData(sPropList, (const char **)sCellData, sCellRows);

View file

@ -9,10 +9,13 @@
#include "../compiler/opcodes.h" #include "../compiler/opcodes.h"
#include <ctype.h> #include <ctype.h>
#include <dirent.h>
#include <fnmatch.h>
#include <math.h> #include <math.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
@ -29,7 +32,9 @@ static BasCallFrameT *currentFrame(BasVmT *vm);
static void defaultPrint(void *ctx, const char *text, bool newline); static void defaultPrint(void *ctx, const char *text, bool newline);
static BasVmResultE execArith(BasVmT *vm, uint8_t op); static BasVmResultE execArith(BasVmT *vm, uint8_t op);
static BasVmResultE execCompare(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 execFileOp(BasVmT *vm, uint8_t op);
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op);
static BasVmResultE execLogical(BasVmT *vm, uint8_t op); static BasVmResultE execLogical(BasVmT *vm, uint8_t op);
static BasVmResultE execMath(BasVmT *vm, uint8_t op); static BasVmResultE execMath(BasVmT *vm, uint8_t op);
static BasVmResultE execPrint(BasVmT *vm); static BasVmResultE execPrint(BasVmT *vm);
@ -318,6 +323,9 @@ void basVmDestroy(BasVmT *vm) {
} }
} }
// Close Dir$ iterator
dirClose();
basStringSystemShutdown(); basStringSystemShutdown();
free(vm); free(vm);
} }
@ -1868,6 +1876,21 @@ BasVmResultE basVmStep(BasVmT *vm) {
case OP_FILE_INPUT_N: case OP_FILE_INPUT_N:
return execFileOp(vm, op); 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 // 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 // execLogical
// ============================================================ // ============================================================

View file

@ -411,6 +411,9 @@ static void scanAppsDir(void) {
// object that the shell can dlopen(). We skip progman.app to avoid listing // object that the shell can dlopen(). We skip progman.app to avoid listing
// ourselves in the launcher grid. // ourselves in the launcher grid.
static void scanAppsDirRecurse(const char *dirPath) { 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); DIR *dir = opendir(dirPath);
if (!dir) { if (!dir) {
@ -421,44 +424,37 @@ static void scanAppsDirRecurse(const char *dirPath) {
return; return;
} }
char **names = NULL;
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(dir)) != NULL) { while ((ent = readdir(dir)) != NULL) {
// Skip . and .. if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue; continue;
} }
// Copy d_name before recursion — readdir may use a shared buffer arrput(names, strdup(ent->d_name));
char name[MAX_PATH_LEN]; }
snprintf(name, sizeof(name), "%s", 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]; 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; struct stat st;
if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) { if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) {
scanAppsDirRecurse(fullPath); scanAppsDirRecurse(fullPath);
free(names[i]);
continue; continue;
} }
int32_t len = strlen(name); int32_t len = (int32_t)strlen(names[i]);
if (len < 5) { if (len < 5 || strcasecmp(names[i] + len - 4, ".app") != 0 || strcasecmp(names[i], "progman.app") == 0) {
continue; free(names[i]);
}
// 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) {
continue; continue;
} }
@ -473,7 +469,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
nameLen = SHELL_APP_NAME_MAX - 1; nameLen = SHELL_APP_NAME_MAX - 1;
} }
memcpy(newEntry.name, name, nameLen); memcpy(newEntry.name, names[i], nameLen);
newEntry.name[nameLen] = '\0'; newEntry.name[nameLen] = '\0';
if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') { 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); newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch);
if (!newEntry.iconData) { 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, "name", newEntry.name, SHELL_APP_NAME_MAX);
dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip)); 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); arrput(sAppFiles, newEntry);
sAppCount = (int32_t)arrlen(sAppFiles); sAppCount = (int32_t)arrlen(sAppFiles);
free(names[i]);
dvxUpdate(sAc); dvxUpdate(sAc);
} }
closedir(dir); arrfree(names);
} }
@ -592,3 +589,8 @@ int32_t appMain(DxeAppContextT *ctx) {
return 0; return 0;
} }
void appShutdown(void) {
shellUnregisterDesktopUpdate(desktopUpdate);
}

View file

@ -22,15 +22,6 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGETDIR = $(LIBSDIR)/kpunch/libdvx TARGETDIR = $(LIBSDIR)/kpunch/libdvx
TARGET = $(TARGETDIR)/libdvx.lib 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 .PHONY: all clean
all: $(TARGET) $(TARGETDIR)/libdvx.dep all: $(TARGET) $(TARGETDIR)/libdvx.dep
@ -39,7 +30,7 @@ $(TARGETDIR)/libdvx.dep: ../config/libdvx.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(TARGETDIR) $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/libdvx.dxe $(DVX_EXPORTS) -U $(OBJS) $(DXE3GEN) -o $(TARGETDIR)/libdvx.dxe -U $(OBJS)
mv $(TARGETDIR)/libdvx.dxe $@ mv $(TARGETDIR)/libdvx.dxe $@
$(OBJDIR)/%.o: %.c | $(OBJDIR) $(OBJDIR)/%.o: %.c | $(OBJDIR)

View file

@ -706,8 +706,8 @@ Each DXE module is compiled to an object file with GCC, then linked with dxe3gen
# Compile # Compile
i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c
# Link as DXE with exported symbols # Link as DXE (exports all non-static symbols)
dxe3gen -o widget.wgt -E _wgtRegister -U widget.o dxe3gen -o widget.wgt -U widget.o
# Optionally append resources # Optionally append resources
dvxres build widget.wgt widget.res dvxres build widget.wgt widget.res

View file

@ -144,7 +144,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win);
static void pollKeyboard(AppContextT *ctx); static void pollKeyboard(AppContextT *ctx);
static void pollMouse(AppContextT *ctx); static void pollMouse(AppContextT *ctx);
static void pollWidgets(AppContextT *ctx); static void pollWidgets(AppContextT *ctx);
static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win);
static void refreshMinimizedIcons(AppContextT *ctx); 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 repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h);
static void updateCursorShape(AppContextT *ctx); static void updateCursorShape(AppContextT *ctx);
@ -950,16 +949,26 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) {
WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target); WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target);
if (next) { if (next) {
WidgetT *prev = sFocusedWidget;
sFocusedWidget = next; 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)) { } else if (wclsHas(target, WGT_METHOD_ON_ACCEL_ACTIVATE)) {
WidgetT *prev = sFocusedWidget;
sFocusedWidget = target; sFocusedWidget = target;
if (prev && prev != target) {
wgtInvalidatePaint(prev);
}
wclsOnAccelActivate(target, win->widgetRoot); wclsOnAccelActivate(target, win->widgetRoot);
wgtInvalidate(win->widgetRoot); wgtInvalidatePaint(target);
} }
return true; return true;
@ -2880,7 +2889,7 @@ static void pollKeyboard(AppContextT *ctx) {
wclsClosePopup(closing); wclsClosePopup(closing);
wgtInvalidate(closing); wgtInvalidatePaint(closing);
continue; continue;
} }
@ -2951,8 +2960,26 @@ static void pollKeyboard(AppContextT *ctx) {
if (next) { if (next) {
sOpenPopup = NULL; sOpenPopup = NULL;
WidgetT *prev = sFocusedWidget;
// Switch focus BEFORE invalidating so paint sees
// the correct focused state for both widgets.
sFocusedWidget = next; 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 // Scroll the widget into view if needed
int32_t scrollX = win->hScroll ? win->hScroll->value : 0; int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
int32_t scrollY = win->vScroll ? win->vScroll->value : 0; int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
@ -2987,8 +3014,6 @@ static void pollKeyboard(AppContextT *ctx) {
break; break;
} }
} }
wgtInvalidate(win->widgetRoot);
} }
arrfree(fstack); arrfree(fstack);
@ -3056,48 +3081,40 @@ static void pollMouse(AppContextT *ctx) {
// rect generation for efficient repainting. // rect generation for efficient repainting.
static void pollWidgets(AppContextT *ctx) { static void pollWidgets(AppContextT *ctx) {
for (int32_t i = 0; i < ctx->stack.count; i++) { for (int32_t i = 0; i < sPollWidgetCount; i++) {
WindowT *win = ctx->stack.windows[i]; WidgetT *w = sPollWidgets[i];
WindowT *win = w->window;
if (win->widgetRoot) { if (!win) {
pollWidgetsWalk(ctx, win->widgetRoot, 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); wclsPoll(w, win);
// If the poll dirtied internal state and the widget supports if (!wclsHas(w, WGT_METHOD_QUICK_REPAINT)) {
// quickRepaint, render the dirty rows directly into the window's continue;
// 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 dirtyY = 0;
int32_t dirtyH = 0; int32_t dirtyH = 0;
if (wclsQuickRepaint(w, &dirtyY, &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 scrollY = win->vScroll ? win->vScroll->value : 0;
int32_t rectX = win->x + win->contentX; int32_t rectX = win->x + win->contentX;
int32_t rectY = win->y + win->contentY + dirtyY - scrollY; int32_t rectY = win->y + win->contentY + dirtyY - scrollY;
int32_t rectW = win->contentW; int32_t rectW = win->contentW;
win->contentDirty = true;
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
} }
} }
} }
for (WidgetT *child = w->firstChild; child; child = child->nextSibling) {
pollWidgetsWalk(ctx, child, win);
}
} }
// ============================================================ // ============================================================
// refreshMinimizedIcons // refreshMinimizedIcons
// ============================================================ // ============================================================
@ -3219,6 +3236,12 @@ static void updateCursorShape(AppContextT *ctx) {
int32_t mx = ctx->mouseX; int32_t mx = ctx->mouseX;
int32_t my = ctx->mouseY; 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 // During active resize, keep the resize cursor
if (ctx->stack.resizeWindow >= 0) { if (ctx->stack.resizeWindow >= 0) {
int32_t edge = ctx->stack.resizeEdge; int32_t edge = ctx->stack.resizeEdge;
@ -5063,14 +5086,23 @@ bool dvxUpdate(AppContextT *ctx) {
refreshMinimizedIcons(ctx); refreshMinimizedIcons(ctx);
} }
// Auto-paint windows that haven't had their first paint yet. // Flush deferred widget paints. wgtInvalidatePaint sets
// This fires one frame after creation, giving the app time to // widgetPaintPending instead of calling dvxInvalidateWindow inline,
// add all widgets before the first onPaint. // 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++) { for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i]; WindowT *win = ctx->stack.windows[i];
if (win->fullRepaint && win->onPaint) { if ((win->widgetPaintPending || win->fullRepaint) && win->onPaint) {
dvxInvalidateWindow(ctx, win); 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); sKeyPressedBtn->y + sKeyPressedBtn->h / 2);
} }
wgtInvalidate(sKeyPressedBtn); wgtInvalidatePaint(sKeyPressedBtn);
sKeyPressedBtn = NULL; sKeyPressedBtn = NULL;
} }

View file

@ -508,6 +508,7 @@ typedef struct WindowT {
bool modal; bool modal;
bool contentDirty; // true when contentBuf has changed since last icon refresh bool contentDirty; // true when contentBuf has changed since last icon refresh
bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones 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 maxW; // maximum width (WM_MAX_FROM_SCREEN = use screen width)
int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height) int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height)
// Pre-maximize geometry is saved so wmRestore() can put the window // Pre-maximize geometry is saved so wmRestore() can put the window

View file

@ -249,6 +249,7 @@ typedef struct WidgetT {
bool readOnly; bool readOnly;
bool swallowTab; // Tab key goes to widget, not focus nav bool swallowTab; // Tab key goes to widget, not focus nav
bool paintDirty; // needs repaint (set by wgtInvalidatePaint) 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 char accelKey; // lowercase accelerator character, 0 if none
// Content offset: mouse event coordinates are adjusted by this amount // Content offset: mouse event coordinates are adjusted by this amount

View file

@ -104,6 +104,8 @@ extern WidgetT *sFocusedWidget;
extern WidgetT *sKeyPressedBtn; extern WidgetT *sKeyPressedBtn;
extern WidgetT *sOpenPopup; extern WidgetT *sOpenPopup;
extern WidgetT *sDragWidget; // widget being dragged (any drag type) extern WidgetT *sDragWidget; // widget being dragged (any drag type)
extern int32_t sPollWidgetCount;
extern WidgetT **sPollWidgets; // stb_ds dynamic array
extern void (*sCursorBlinkFn)(void); extern void (*sCursorBlinkFn)(void);
// ============================================================ // ============================================================
@ -172,6 +174,8 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font);
// ============================================================ // ============================================================
void widgetManageScrollbars(WindowT *win, AppContextT *ctx); 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 widgetOnKey(WindowT *win, int32_t key, int32_t mod);
void widgetOnKeyUp(WindowT *win, int32_t scancode, 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); void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);

View file

@ -2845,6 +2845,60 @@ MenuT *wmCreateMenu(void) {
// Unlike freeMenuRecursive (which only frees submenu children because the // Unlike freeMenuRecursive (which only frees submenu children because the
// top-level struct is embedded), this also frees the root MenuT itself. // 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) { void wmFreeMenu(MenuT *menu) {
if (!menu) { if (!menu) {
return; return;

View file

@ -227,4 +227,10 @@ MenuT *wmCreateMenu(void);
// heap-allocated submenu children recursively. // heap-allocated submenu children recursively.
void wmFreeMenu(MenuT *menu); 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 #endif // DVX_WM_H

View file

@ -43,6 +43,7 @@
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#include <fcntl.h> #include <fcntl.h>
#include <fnmatch.h>
#include <sys/dxe.h> #include <sys/dxe.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/time.h> #include <sys/time.h>
@ -2551,6 +2552,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
// --- filesystem --- // --- filesystem ---
DXE_EXPORT(access) DXE_EXPORT(access)
DXE_EXPORT(chdir) DXE_EXPORT(chdir)
DXE_EXPORT(chmod)
DXE_EXPORT(fnmatch)
DXE_EXPORT(getcwd) DXE_EXPORT(getcwd)
DXE_EXPORT(realpath) DXE_EXPORT(realpath)
DXE_EXPORT(stat) DXE_EXPORT(stat)

View file

@ -48,6 +48,8 @@ bool sDebugLayout = false;
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk) WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
WidgetT *sDragWidget = NULL; // widget being dragged (any drag type) 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. // Shared clipboard -- process-wide, not per-widget.
static char *sClipboard = NULL; static char *sClipboard = NULL;
@ -196,6 +198,11 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) {
w->visible = true; w->visible = true;
w->enabled = true; w->enabled = true;
if (w->wclass->flags & WCLASS_NEEDS_POLL) {
arrput(sPollWidgets, w);
sPollWidgetCount = (int32_t)arrlen(sPollWidgets);
}
if (parent) { if (parent) {
w->window = parent->window; w->window = parent->window;
widgetAddChild(parent, w); widgetAddChild(parent, w);

View file

@ -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); wclsGetPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH);
if (x >= popX && x < popX + popW && y >= popY && y < popY + 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); wclsOnMouse(sOpenPopup, root, x, y);
wgtInvalidatePaint(root);
sOpenPopup = NULL;
wgtInvalidate(root);
return; return;
} }
@ -368,14 +368,14 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
return; return;
} }
// Clear focus from the previously focused widget. This is done via // Clear focus from the previously focused widget. Must set
// the cached sFocusedWidget pointer rather than walking the tree to // sFocusedWidget to NULL BEFORE invalidating so the inline paint
// find the focused widget -- an O(1) operation vs O(n). // sees the widget as unfocused and erases its highlight.
WidgetT *prevFocus = sFocusedWidget; WidgetT *prevFocus = sFocusedWidget;
if (sFocusedWidget) { if (sFocusedWidget) {
wgtInvalidatePaint(sFocusedWidget);
sFocusedWidget = NULL; sFocusedWidget = NULL;
wgtInvalidatePaint(prevFocus);
} }
// Dispatch to the hit widget's mouse handler via vtable. The handler // 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) { if (sFocusedWidget && sFocusedWidget != prevFocus && sFocusedWidget->onFocus) {
sFocusedWidget->onFocus(sFocusedWidget); 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;
} }

View file

@ -13,6 +13,7 @@
#include "dvxWgtP.h" #include "dvxWgtP.h"
#include "dvxPlat.h" #include "dvxPlat.h"
#include "stb_ds_wrap.h"
#include "../widgets/box/box.h" #include "../widgets/box/box.h"
static bool sFullRepaint = false; 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 // The window's fullRepaint flag is stored in sFullRepaint for the
// duration of the paint walk. // duration of the paint walk.
bool dirty = w->paintDirty || sFullRepaint; bool dirty = w->paintDirty || sFullRepaint;
w->paintDirty = false;
if (dirty) { // For WCLASS_PAINTS_CHILDREN widgets (TabControl, TreeView, ScrollPane,
wclsPaint(w, d, ops, font, colors); // 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;
} }
// Widgets that paint their own children return early // Skip entirely if nothing needs painting in this subtree
if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) { 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;
if (sDebugLayout && dirty) { if (sDebugLayout && dirty) {
debugContainerBorder(w, d, ops); debugContainerBorder(w, d, ops);
} }
@ -96,6 +121,12 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
return; return;
} }
w->paintDirty = false;
if (dirty) {
wclsPaint(w, d, ops, font, colors);
}
// Always recurse into children — a clean parent may have dirty children // Always recurse into children — a clean parent may have dirty children
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
widgetPaintOne(c, d, ops, font, colors); widgetPaintOne(c, d, ops, font, colors);
@ -192,6 +223,16 @@ void wgtDestroy(WidgetT *w) {
sDragWidget = NULL; 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 this is the root, clear the window's reference
if (w->window && w->window->widgetRoot == w) { if (w->window && w->window->widgetRoot == w) {
w->window->widgetRoot = NULL; w->window->widgetRoot = NULL;
@ -318,6 +359,8 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) {
win->onKey = widgetOnKey; win->onKey = widgetOnKey;
win->onKeyUp = widgetOnKeyUp; win->onKeyUp = widgetOnKeyUp;
win->onResize = widgetOnResize; win->onResize = widgetOnResize;
win->onBlur = widgetOnBlur;
win->onFocus = widgetOnFocus;
return root; return root;
} }
@ -386,20 +429,22 @@ void wgtInvalidatePaint(WidgetT *w) {
// Mark only this widget as needing repaint // Mark only this widget as needing repaint
w->paintDirty = true; w->paintDirty = true;
// Propagate childDirty up through WCLASS_PAINTS_CHILDREN ancestors
// so they know to recurse into children during partial repaints.
WidgetT *root = w; WidgetT *root = w;
while (root->parent) { while (root->parent) {
root = root->parent; root = root->parent;
if (root->wclass && (root->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
root->childDirty = true;
}
} }
AppContextT *ctx = (AppContextT *)root->userData; // Defer the actual paint — it will happen once in the main loop
// before compositing, batching multiple invalidations into one
if (!ctx) { // tree walk instead of one per call.
return; w->window->widgetPaintPending = true;
}
// Partial repaint — only dirty widgets will be repainted
dvxInvalidateWindow(ctx, w->window);
} }

View file

@ -1186,8 +1186,8 @@ img { max-width: 100%; }
<pre><code> # Compile <pre><code> # Compile
i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c i586-pc-msdosdjgpp-gcc -O2 -march=i486 -mtune=i586 -c -o widget.o widget.c
# Link as DXE with exported symbols # Link as DXE (exports all non-static symbols)
dxe3gen -o widget.wgt -E _wgtRegister -U widget.o dxe3gen -o widget.wgt -U widget.o
# Optionally append resources # Optionally append resources
dvxres build widget.wgt widget.res</code></pre> dvxres build widget.wgt widget.res</code></pre>

View file

@ -24,7 +24,7 @@ $(TARGETDIR)/listhelp.dep: ../config/listhelp.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(TARGETDIR) $(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 $@ mv $(TARGETDIR)/listhelp.dxe $@
$(OBJDIR)/%.o: %.c | $(OBJDIR) $(OBJDIR)/%.o: %.c | $(OBJDIR)

View file

@ -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 // 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. // 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) { 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; BevelStyleT bevel;
bevel.highlight = colors->windowHighlight; bevel.highlight = colors->windowHighlight;
bevel.shadow = colors->windowShadow; bevel.shadow = colors->windowShadow;
bevel.face = colors->contentBg; bevel.face = colors->contentBg;
bevel.width = 2; bevel.width = 2;
drawBevel(d, ops, popX, popY, popW, popH, &bevel); drawBevel(d, ops, popX, popY, listW, popH, &bevel);
// Draw items // Draw items
int32_t visibleItems = popH / font->charHeight;
int32_t textX = popX + TEXT_INPUT_PAD; int32_t textX = popX + TEXT_INPUT_PAD;
int32_t textY = popY + 2; int32_t textY = popY + 2;
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; int32_t textW = listW - TEXT_INPUT_PAD * 2 - 4;
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
int32_t idx = scrollPos + 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); drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
} }
// Draw scroll indicators if the list extends beyond visible area // Draw scrollbar
if (itemCount > visibleItems) { if (hasScrollbar) {
int32_t cx = popX + popW / 2; wmDrawVScrollbarAt(d, ops, colors, popX + listW, popY, popH, scrollPos, visibleItems, itemCount);
uint32_t arrowC = colors->menuHighlightBg; }
}
// 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;
}
int32_t thumbPos = 0;
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;
}
} }
} }
// Down triangle (wide at top, point at bottom) return true;
if (scrollPos + visibleItems < itemCount) {
int32_t by = popY + popH - 4;
for (int32_t i = 0; i < 3; i++) {
drawHLine(d, ops, cx - i, by - i, 1 + i * 2, arrowC);
}
}
}
} }
@ -193,6 +273,11 @@ void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t conten
*popW = w->w; *popW = w->w;
*popH = visibleItems * font->charHeight + 4; *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) { if (w->y + w->h + *popH <= contentH) {
*popY = w->y + w->h; *popY = w->y + w->h;
} else { } else {

View file

@ -11,6 +11,7 @@
#define DROPDOWN_BTN_WIDTH 16 #define DROPDOWN_BTN_WIDTH 16
#define DROPDOWN_MAX_VISIBLE 8 #define DROPDOWN_MAX_VISIBLE 8
#define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH
// ============================================================ // ============================================================
// Dropdown arrow glyph // 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); 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 #endif // LIST_HELP_H

View file

@ -227,41 +227,44 @@ static void readDeps(ModuleT *mod) {
// ============================================================ // ============================================================
static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) { 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); DIR *dir = opendir(dirPath);
if (!dir) { if (!dir) {
return; return;
} }
char **names = NULL;
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(dir)) != NULL) { while ((ent = readdir(dir)) != NULL) {
// Copy d_name — readdir may use a shared buffer across recursion if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
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'))) {
continue; 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]; 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 = (int32_t)strlen(names[i]);
int32_t nameLen = strlen(name);
int32_t extLen = strlen(ext);
if (nameLen > extLen && strcasecmp(name + nameLen - extLen, ext) == 0) { if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) {
ModuleT mod; ModuleT mod;
memset(&mod, 0, sizeof(mod)); memset(&mod, 0, sizeof(mod));
snprintf(mod.path, sizeof(mod.path), "%s", path); snprintf(mod.path, sizeof(mod.path), "%s", path);
extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName)); extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName));
arrput(*mods, mod); arrput(*mods, mod);
continue; } else {
}
// Recurse into subdirectories
struct stat st; struct stat st;
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
@ -269,7 +272,10 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
} }
} }
closedir(dir); free(names[i]);
}
arrfree(names);
} }
@ -500,13 +506,13 @@ bool platformGlobMatch(const char *pattern, const char *name) {
// Recursively count .hcf files under the given directory // Recursively count .hcf files under the given directory
static int32_t countHcfFilesRecurse(const char *dirPath) { static int32_t countHcfFilesRecurse(const char *dirPath) {
int32_t count = 0;
DIR *dir = opendir(dirPath); DIR *dir = opendir(dirPath);
if (!dir) { if (!dir) {
return 0; return 0;
} }
char **names = NULL;
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(dir)) != NULL) { while ((ent = readdir(dir)) != NULL) {
@ -514,31 +520,36 @@ static int32_t countHcfFilesRecurse(const char *dirPath) {
continue; continue;
} }
// Copy d_name before any recursion — readdir may use a shared buffer arrput(names, strdup(ent->d_name));
char name[DVX_MAX_PATH]; }
snprintf(name, sizeof(name), "%s", 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]; char fullPath[DVX_MAX_PATH];
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]);
struct stat st; struct stat st;
if (stat(fullPath, &st) != 0) { if (stat(fullPath, &st) == 0) {
continue;
}
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
count += countHcfFilesRecurse(fullPath); count += countHcfFilesRecurse(fullPath);
} else { } else {
int32_t nameLen = (int32_t)strlen(name); int32_t nameLen = (int32_t)strlen(names[i]);
if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) { if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
count++; count++;
} }
} }
} }
closedir(dir); free(names[i]);
}
arrfree(names);
return count; return count;
} }
@ -580,6 +591,7 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
return; return;
} }
char **names = NULL;
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(d)) != NULL) { while ((ent = readdir(d)) != NULL) {
@ -587,32 +599,35 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
continue; continue;
} }
char name[DVX_MAX_PATH]; arrput(names, strdup(ent->d_name));
snprintf(name, sizeof(name), "%s", 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]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, name); snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, names[i]);
struct stat st; struct stat st;
if (stat(fullPath, &st) != 0) { if (stat(fullPath, &st) == 0) {
continue;
}
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
char subPattern[DVX_MAX_PATH]; char subPattern[DVX_MAX_PATH];
snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart); snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart);
writeGlobToResp(resp, subPattern, excludePattern); writeGlobToResp(resp, subPattern, excludePattern);
} else if (platformGlobMatch(globPart, name)) { } else if (platformGlobMatch(globPart, names[i])) {
if (excludePattern && platformGlobMatch(excludePattern, name)) { if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
continue;
}
fprintf(resp, "%s\n", fullPath); fprintf(resp, "%s\n", fullPath);
} }
} }
}
closedir(d); free(names[i]);
}
arrfree(names);
} }
@ -728,6 +743,7 @@ static void processHcfDir(const char *dirPath) {
return; return;
} }
char **names = NULL;
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(dir)) != NULL) { while ((ent = readdir(dir)) != NULL) {
@ -735,25 +751,26 @@ static void processHcfDir(const char *dirPath) {
continue; continue;
} }
// Copy d_name before any recursion — readdir may use a shared buffer arrput(names, strdup(ent->d_name));
char name[DVX_MAX_PATH]; }
snprintf(name, sizeof(name), "%s", 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]; char fullPath[DVX_MAX_PATH];
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]);
struct stat st; struct stat st;
if (stat(fullPath, &st) != 0) { if (stat(fullPath, &st) == 0) {
continue;
}
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
processHcfDir(fullPath); processHcfDir(fullPath);
} else { } else {
int32_t nameLen = (int32_t)strlen(name); int32_t nameLen = (int32_t)strlen(names[i]);
if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) { if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
processHcf(fullPath, dirPath); processHcf(fullPath, dirPath);
sSplashLoaded++; sSplashLoaded++;
splashUpdateProgress(); splashUpdateProgress();
@ -761,7 +778,10 @@ static void processHcfDir(const char *dirPath) {
} }
} }
closedir(dir); free(names[i]);
}
arrfree(names);
} }

4
run.sh
View file

@ -1,5 +1,5 @@
#!/bin/bash #!/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

View file

@ -33,7 +33,7 @@ Building an Application
1. Write your app with appDescriptor and appMain exports 1. Write your app with appDescriptor and appMain exports
2. Compile: gcc -c -o myapp.o myapp.c -Isdk/include/core ... 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 4. Optionally create a .res file and build resources with dvxres
See samples/hello/ for a complete example. See samples/hello/ for a complete example.
@ -43,7 +43,7 @@ Building a Widget
1. Write your widget with wgtRegister export 1. Write your widget with wgtRegister export
2. Compile: gcc -c -o mywgt.o mywgt.c -Isdk/include/core 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. See samples/widget/ for a complete example.

View file

@ -18,7 +18,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 \
all: hello.app all: hello.app
hello.app: hello.o hello.res icon32.bmp 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 $(DVXRES) build $@ hello.res
hello.o: hello.c hello.o: hello.c

View file

@ -13,7 +13,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586
all: mylib.lib all: mylib.lib
mylib.lib: mylib.o mylib.lib: mylib.o
$(DXE3GEN) -o mylib.dxe -E _myLibAdd -E _myLibMul -E _myLibVersion -U $< $(DXE3GEN) -o mylib.dxe -U $<
mv mylib.dxe $@ mv mylib.dxe $@
mylib.o: mylib.c mylib.h mylib.o: mylib.c mylib.h

View file

@ -2,7 +2,7 @@
// //
// Build: // Build:
// i586-pc-msdosdjgpp-gcc -O2 -Wall -c -o mylib.o mylib.c // 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/<vendor>/MYLIB/ on the target. // Deploy: copy mylib.lib to LIBS/<vendor>/MYLIB/ on the target.
// Add a mylib.dep file if this library depends on others. // Add a mylib.dep file if this library depends on others.

View file

@ -16,7 +16,7 @@ CFLAGS = -O2 -Wall -Wextra -Werror -march=i486 -mtune=i586 \
all: mywgt.wgt all: mywgt.wgt
mywgt.wgt: mywgt.o mywgt.res icon24.bmp 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 $@ mv mywgt.dxe $@
$(DVXRES) build $@ mywgt.res $(DVXRES) build $@ mywgt.res

View file

@ -8,7 +8,7 @@
// //
// Build: // Build:
// i586-pc-msdosdjgpp-gcc -O2 -Wall -I../../include/core -c -o mywgt.o mywgt.c // 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/<vendor>/MYWGT/ on the target. // Deploy: copy mywgt.wgt to WIDGETS/<vendor>/MYWGT/ on the target.
// Optionally include MYWGT.DHS (C API docs) and MYWGT.BHS (BASIC docs). // Optionally include MYWGT.DHS (C API docs) and MYWGT.BHS (BASIC docs).

View file

@ -24,9 +24,7 @@ $(TARGETDIR)/serial.dep: ../config/serial.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(TARGETDIR) $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/serial.dxe \ $(DXE3GEN) -o $(TARGETDIR)/serial.dxe -U $(OBJS)
-E _rs232 -E _pkt -E _secLink -E _secDh -E _secCipher -E _secRng \
-U $(OBJS)
mv $(TARGETDIR)/serial.dxe $@ mv $(TARGETDIR)/serial.dxe $@
$(OBJDIR)/rs232.o: ../rs232/rs232.c | $(OBJDIR) $(OBJDIR)/rs232.o: ../rs232/rs232.c | $(OBJDIR)

View file

@ -29,7 +29,7 @@ $(TARGETDIR)/dvxshell.dep: ../config/dvxshell.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(TARGETDIR) $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/dvxshell.dxe -E _shell -U $(OBJS) $(DXE3GEN) -o $(TARGETDIR)/dvxshell.dxe -U $(OBJS)
mv $(TARGETDIR)/dvxshell.dxe $@ mv $(TARGETDIR)/dvxshell.dxe $@
$(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR) $(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR)

View file

@ -99,9 +99,7 @@ $(OBJDIR)/sqlite_opcodes.o: $(GEN_OPCODES_C) | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $< $(CC) $(CFLAGS) -c -o $@ $<
$(TARGET): $(OBJS) | $(TARGETDIR) $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/dvxsql.dxe \ $(DXE3GEN) -o $(TARGETDIR)/dvxsql.dxe -U $(OBJS)
-E _dvxSql -E _sqlite3 \
-U $(OBJS)
mv $(TARGETDIR)/dvxsql.dxe $@ mv $(TARGETDIR)/dvxsql.dxe $@
$(OBJDIR): $(OBJDIR):

View file

@ -20,7 +20,7 @@ TARGET = $(TARGETDIR)/libtasks.lib
all: $(TARGET) all: $(TARGET)
$(TARGET): $(OBJS) | $(TARGETDIR) $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -E _ts -U $(OBJS) $(DXE3GEN) -o $(TARGETDIR)/libtasks.dxe -U $(OBJS)
mv $(TARGETDIR)/libtasks.dxe $@ mv $(TARGETDIR)/libtasks.dxe $@
$(OBJDIR)/%.o: %.c | $(OBJDIR) $(OBJDIR)/%.o: %.c | $(OBJDIR)

View file

@ -24,7 +24,7 @@ $(TARGETDIR)/texthelp.dep: ../config/texthelp.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@ sed 's/$$/\r/' $< > $@
$(TARGET): $(OBJS) | $(TARGETDIR) $(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 $@ mv $(TARGETDIR)/texthelp.dxe $@
$(OBJDIR)/%.o: %.c | $(OBJDIR) $(OBJDIR)/%.o: %.c | $(OBJDIR)

View file

@ -51,7 +51,6 @@ WGT_MODS = $(foreach n,$(WGT_NAMES),$(WGTDIR)/$(n)/$(n).wgt)
OBJS = $(foreach w,$(WIDGETS),$(OBJDIR)/$(word 3,$(subst :, ,$w)).o) OBJS = $(foreach w,$(WIDGETS),$(OBJDIR)/$(word 3,$(subst :, ,$w)).o)
# Per-widget extra DXE3GEN flags (e.g. additional -E exports for dlsym) # 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 DEPFILES = textinpt combobox spinner terminal listbox dropdown listview treeview
WGT_DEPS = $(foreach d,$(DEPFILES),$(WGTDIR)/$(d)/$(d).dep) 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 $$@ $$< $$(CC) $$(CFLAGS) -c -o $$@ $$<
$(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(WGTDIR)/$(word 1,$(subst :, ,$1)) $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).wgt: $(OBJDIR)/$(word 3,$(subst :, ,$1)).o | $(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 $$@ mv $(WGTDIR)/$(word 1,$(subst :, ,$1))/$(word 1,$(subst :, ,$1)).dxe $$@
@if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \ @if [ -f $(word 2,$(subst :, ,$1))/$(word 4,$(subst :, ,$1)).res ]; then \
cd $(word 2,$(subst :, ,$1)) && ../$(DVXRES) build ../$$@ $(word 4,$(subst :, ,$1)).res; \ cd $(word 2,$(subst :, ,$1)) && ../$(DVXRES) build ../$$@ $(word 4,$(subst :, ,$1)).res; \

View file

@ -695,23 +695,27 @@ void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
return; 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; BevelStyleT sunken;
sunken.highlight = colors->windowShadow; sunken.highlight = colors->windowShadow;
sunken.shadow = colors->windowHighlight; sunken.shadow = colors->windowHighlight;
sunken.face = 0; sunken.face = 0;
sunken.width = CANVAS_BORDER; 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 // Blit the canvas data inside the border
int32_t imgW = cd->canvasW; rectCopy(d, ops, w->x + CANVAS_BORDER, w->y + CANVAS_BORDER,
int32_t imgH = cd->canvasH;
int32_t dx = w->x + CANVAS_BORDER;
int32_t dy = w->y + CANVAS_BORDER;
rectCopy(d, ops, dx, dy,
cd->pixelData, cd->canvasPitch, cd->pixelData, cd->canvasPitch,
0, 0, imgW, imgH); 0, 0, cd->canvasW, cd->canvasH);
} }

View file

@ -129,6 +129,9 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; 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 // Draw checkbox box
BevelStyleT bevel; BevelStyleT bevel;
bevel.highlight = colors->windowShadow; bevel.highlight = colors->windowShadow;
@ -166,7 +169,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
} }
if (w == sFocusedWidget) { 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);
} }
} }

View file

@ -166,6 +166,24 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
return; 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 // 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; sFocusedWidget = w;
ComboBoxDataT *d = (ComboBoxDataT *)w->data; 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) { if (d->open) {
AppContextT *ctx = wgtGetContext(w); AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font; 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); 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; int32_t itemIdx = d->listScrollPos + (vy - popY - 2) / font->charHeight;
if (itemIdx >= 0 && itemIdx < d->itemCount) { if (itemIdx >= 0 && itemIdx < d->itemCount) {

View file

@ -135,6 +135,18 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->onChange) { if (w->onChange) {
w->onChange(w); 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 { } else {
// Popup is closed // Popup is closed
@ -163,6 +175,17 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (d->selectedIdx > 0) { if (d->selectedIdx > 0) {
d->selectedIdx--; 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) { if (w->onChange) {
w->onChange(w); w->onChange(w);
} }
@ -190,7 +213,7 @@ void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
sFocusedWidget = w; sFocusedWidget = w;
DropdownDataT *d = (DropdownDataT *)w->data; 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) { if (d->open) {
AppContextT *ctx = wgtGetContext(w); AppContextT *ctx = wgtGetContext(w);
const BitmapFontT *font = &ctx->font; 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); 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; int32_t itemIdx = d->scrollPos + (vy - popY - 2) / font->charHeight;
if (itemIdx >= 0 && itemIdx < d->itemCount) { if (itemIdx >= 0 && itemIdx < d->itemCount) {

View file

@ -233,6 +233,11 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t newSel = widgetNavigateIndex(key, sel, d->itemCount, visibleRows); 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) { if (newSel < 0) {
return; return;
} }

View file

@ -483,6 +483,34 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, visibleRows); 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) { if (newDisplaySel < 0) {
return; return;
} }

View file

@ -230,6 +230,9 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; 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 // Draw diamond-shaped radio box
int32_t bx = w->x; int32_t bx = w->x;
int32_t mid = CHECKBOX_BOX_SIZE / 2; 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) { 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);
} }
} }

View file

@ -791,19 +791,22 @@ void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV);
sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH);
// Sunken border bool selfDirty = w->paintDirty;
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w));
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
// Clip to content area and paint children // Clip to content area and paint children
int32_t oldClipX = d->clipX; int32_t oldClipX = d->clipX;
int32_t oldClipY = d->clipY; int32_t oldClipY = d->clipY;
int32_t oldClipW = d->clipW; int32_t oldClipW = d->clipW;
int32_t oldClipH = d->clipH; int32_t oldClipH = d->clipH;
setClipRect(d, w->x + spBorder(w), w->y + spBorder(w), innerW, innerH);
// Fill background 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); 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) // Paint children (already positioned by layout with scroll offset)
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { 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); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
if (!selfDirty) {
return;
}
// Draw scrollbars // Draw scrollbars
if (needVSb) { if (needVSb) {
int32_t sbX = w->x + w->w - spBorder(w) - SP_SB_W; int32_t sbX = w->x + w->w - spBorder(w) - SP_SB_W;

View file

@ -202,9 +202,13 @@ void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
(void)font; (void)font;
SliderDataT *sd = (SliderDataT *)w->data; SliderDataT *sd = (SliderDataT *)w->data;
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; 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 tickFg = w->enabled ? fg : colors->windowShadow;
uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough; 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; int32_t range = sd->maxValue - sd->minValue;
if (range <= 0) { if (range <= 0) {

View file

@ -352,6 +352,8 @@ void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
if (hit->onChange) { if (hit->onChange) {
hit->onChange(hit); hit->onChange(hit);
} }
wgtInvalidate(hit);
} }
break; break;
@ -386,6 +388,11 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
TabControlDataT *td = (TabControlDataT *)w->data; TabControlDataT *td = (TabControlDataT *)w->data;
int32_t tabH = font->charHeight + TAB_PAD_V * 2; int32_t tabH = font->charHeight + TAB_PAD_V * 2;
bool scroll = tabNeedScroll(w, font); bool scroll = tabNeedScroll(w, font);
bool selfDirty = w->paintDirty;
if (!selfDirty) {
goto paintChildren;
}
// Content panel // Content panel
BevelStyleT panelBevel; BevelStyleT panelBevel;
@ -505,15 +512,17 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
paintChildren:
;
// Paint only active tab page's children // Paint only active tab page's children
tabIdx = 0; int32_t activeIdx = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type != sTabPageTypeId) { if (c->type != sTabPageTypeId) {
continue; continue;
} }
if (tabIdx == td->activeTab) { if (activeIdx == td->activeTab) {
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) { for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
widgetPaintOne(gc, d, ops, font, colors); widgetPaintOne(gc, d, ops, font, colors);
} }
@ -521,7 +530,7 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
break; break;
} }
tabIdx++; activeIdx++;
} }
} }

View file

@ -31,6 +31,7 @@ typedef struct {
int32_t (*getCursorLine)(const WidgetT *w); int32_t (*getCursorLine)(const WidgetT *w);
void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t));
int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize);
void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count);
} TextInputApiT; } TextInputApiT;
static inline const TextInputApiT *dvxTextInputApi(void) { static inline const TextInputApiT *dvxTextInputApi(void) {
@ -56,5 +57,6 @@ static inline const TextInputApiT *dvxTextInputApi(void) {
#define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w) #define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w)
#define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn) #define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn)
#define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz) #define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz)
#define wgtTextAreaSetSyntaxColors(w, colors, count) dvxTextInputApi()->setSyntaxColors(w, colors, count)
#endif // TEXTINPT_H #endif // TEXTINPT_H

View file

@ -61,6 +61,16 @@
static int32_t sTextInputTypeId = -1; static int32_t sTextInputTypeId = -1;
static int32_t sTextAreaTypeId = -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 { typedef enum {
InputNormalE, InputNormalE,
InputPasswordE, InputPasswordE,
@ -137,6 +147,9 @@ typedef struct {
// Gutter click callback (optional). Fired when user clicks in the gutter. // Gutter click callback (optional). Fired when user clicks in the gutter.
void (*onGutterClick)(WidgetT *w, int32_t lineNum); 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) // Pre-allocated paint buffers (avoid 3KB stack alloc per visible line per frame)
uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN) uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN)
char *expandBuf; // tab-expanded text (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) // Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c)
#define CURSOR_BLINK_MS 250 #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 // 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 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 ColorSchemeT *colors); 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 bool maskCharValid(char slot, char ch);
static int32_t maskFirstSlot(const char *mask); static int32_t maskFirstSlot(const char *mask);
static bool maskIsSlot(char ch); static bool maskIsSlot(char ch);
@ -2046,16 +2049,19 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
// syntaxColor // syntaxColor
// ============================================================ // ============================================================
static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors) { static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) {
(void)colors; 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) { switch (idx) {
case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128); // dark blue case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128);
case SYNTAX_STRING: return packColor(d, 128, 0, 0); // dark red case SYNTAX_STRING: return packColor(d, 128, 0, 0);
case SYNTAX_COMMENT: return packColor(d, 0, 128, 0); // dark green case SYNTAX_COMMENT: return packColor(d, 0, 128, 0);
case SYNTAX_NUMBER: return packColor(d, 128, 0, 128); // purple case SYNTAX_NUMBER: return packColor(d, 128, 0, 128);
case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0); // dark yellow case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0);
case SYNTAX_TYPE: return packColor(d, 0, 128, 128); // teal case SYNTAX_TYPE: return packColor(d, 0, 128, 128);
default: return defaultFg; 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 // Draw text with per-character syntax coloring. Batches consecutive
// characters of the same color into single drawTextN calls. // 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; int32_t runStart = 0;
while (runStart < len) { while (runStart < len) {
@ -2079,7 +2085,7 @@ static void drawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFont
runEnd++; 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); drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true);
runStart = runEnd; runStart = runEnd;
} }
@ -2300,7 +2306,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
// Before selection // Before selection
if (drawStart < vSelLo) { if (drawStart < vSelLo) {
if (hasSyntax) { 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 { } else {
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true); 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 // After selection
if (vSelHi < drawEnd) { if (vSelHi < drawEnd) {
if (hasSyntax) { 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 { } else {
drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true); 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 // No selection on this line
if (drawStart < drawEnd) { if (drawStart < drawEnd) {
if (hasSyntax) { 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 { } else {
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true); 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) { int32_t wgtTextAreaGetCursorLine(const WidgetT *w) {
if (!w || w->type != sTextAreaTypeId) { if (!w || w->type != sTextAreaTypeId) {
return 1; return 1;
@ -3395,6 +3421,7 @@ static const struct {
int32_t (*getCursorLine)(const WidgetT *w); int32_t (*getCursorLine)(const WidgetT *w);
void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t));
int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize);
void (*setSyntaxColors)(WidgetT *w, const uint32_t *colors, int32_t count);
} sApi = { } sApi = {
.create = wgtTextInput, .create = wgtTextInput,
.password = wgtPasswordInput, .password = wgtPasswordInput,
@ -3412,7 +3439,8 @@ static const struct {
.setLineDecorator = wgtTextAreaSetLineDecorator, .setLineDecorator = wgtTextAreaSetLineDecorator,
.getCursorLine = wgtTextAreaGetCursorLine, .getCursorLine = wgtTextAreaGetCursorLine,
.setGutterClick = wgtTextAreaSetGutterClickCallback, .setGutterClick = wgtTextAreaSetGutterClickCallback,
.getWordAtCursor = wgtTextAreaGetWordAtCursor .getWordAtCursor = wgtTextAreaGetWordAtCursor,
.setSyntaxColors = wgtTextAreaSetSyntaxColors
}; };
// Per-type APIs for the designer // Per-type APIs for the designer

View file

@ -891,6 +891,63 @@ void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) {
selTi->selected = !selTi->selected; selTi->selected = !selTi->selected;
tv->anchorItem = sel; 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 { } else {
return; return;
} }