An insane number of performance, logic, and feature enhancements; bug fixess; and other things.

This commit is contained in:
Scott Duensing 2026-04-12 22:26:18 -05:00
parent 3c886a97f6
commit 454a3620f7
53 changed files with 3355 additions and 858 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@ dosbench/
bin/ bin/
obj/ obj/
lib/ lib/
*~
*.~ *.~
.gitignore~ .gitignore~
.gitattributes~ .gitattributes~

View file

@ -18,7 +18,6 @@ OBJDIR = ../../obj/dvxbasic
LIBSDIR = ../../bin/libs LIBSDIR = ../../bin/libs
APPDIR = ../../bin/apps/kpunch/dvxbasic APPDIR = ../../bin/apps/kpunch/dvxbasic
DVXRES = ../../bin/host/dvxres DVXRES = ../../bin/host/dvxres
SAMPLES = samples/hello.bas samples/formtest.bas samples/clickme.bas samples/clickme.frm samples/input.bas
# Runtime library objects (VM + values) # Runtime library objects (VM + values)
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o
@ -54,7 +53,7 @@ TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/co
.PHONY: all clean tests .PHONY: all clean tests
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(APP_TARGET) install-samples all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(APP_TARGET)
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK)
@ -85,8 +84,6 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR)
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $(COMP_OBJS) $(APP_OBJS) $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $(COMP_OBJS) $(APP_OBJS)
$(DVXRES) build $@ dvxbasic.res $(DVXRES) build $@ dvxbasic.res
install-samples: $(SAMPLES) | $(APPDIR)
cp $(SAMPLES) $(APPDIR)/
# Object files # Object files
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR) $(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)

View file

@ -689,7 +689,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
// If it's a keyword and has no suffix, return the keyword token. // If it's a keyword and has no suffix, return the keyword token.
// String-returning builtins (SQLError$, SQLField$) also match with $. // String-returning builtins (SQLError$, SQLField$) also match with $.
if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_SQLERROR || kwType == TOK_SQLFIELD)) { if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_SQLERROR || kwType == TOK_SQLFIELD || kwType == TOK_INPUTBOX)) {
return kwType; return kwType;
} }

View file

@ -296,6 +296,7 @@ static void advance(BasParserT *p) {
if (p->hasError) { if (p->hasError) {
return; return;
} }
p->prevLine = p->lex.token.line;
basLexerNext(&p->lex); basLexerNext(&p->lex);
if (p->lex.token.type == TOK_ERROR) { if (p->lex.token.type == TOK_ERROR) {
error(p, p->lex.error); error(p, p->lex.error);
@ -344,9 +345,17 @@ static void error(BasParserT *p, const char *msg) {
if (p->hasError) { if (p->hasError) {
return; return;
} }
p->hasError = true; p->hasError = true;
p->errorLine = p->lex.token.line;
snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)p->lex.token.line, msg); // If the current token is on a later line than the previous token,
// the error is about the previous line (e.g. missing token at EOL).
int32_t line = p->lex.token.line;
if (p->prevLine > 0 && line > p->prevLine) {
line = p->prevLine;
}
p->errorLine = line;
snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)line, msg);
} }
@ -1739,10 +1748,10 @@ static void parsePrimary(BasParserT *p) {
return; return;
} }
// Not a UDT -- treat as control property read: CtrlName.Property // Not a UDT -- treat as control property/method: CtrlName.Member
advance(p); // consume DOT advance(p); // consume DOT
if (!check(p, TOK_IDENT)) { if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property name"); errorExpected(p, "property or method name");
return; return;
} }
char memberName[BAS_MAX_TOKEN_LEN]; char memberName[BAS_MAX_TOKEN_LEN];
@ -1758,11 +1767,32 @@ static void parsePrimary(BasParserT *p) {
basEmitU16(&p->cg, ctrlNameIdx); basEmitU16(&p->cg, ctrlNameIdx);
basEmit8(&p->cg, OP_FIND_CTRL); basEmit8(&p->cg, OP_FIND_CTRL);
// Push property name, LOAD_PROP // If followed by '(', this is a method call with args
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); if (check(p, TOK_LPAREN)) {
basEmit8(&p->cg, OP_PUSH_STR); advance(p); // consume '('
basEmitU16(&p->cg, propNameIdx); uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_LOAD_PROP); basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, methodNameIdx);
int32_t argc = 0;
if (!check(p, TOK_RPAREN)) {
parseExpression(p);
argc++;
while (match(p, TOK_COMMA)) {
parseExpression(p);
argc++;
}
}
expect(p, TOK_RPAREN);
basEmit8(&p->cg, OP_CALL_METHOD);
basEmit8(&p->cg, (uint8_t)argc);
} else {
// Property read
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
basEmit8(&p->cg, OP_PUSH_STR);
basEmitU16(&p->cg, propNameIdx);
basEmit8(&p->cg, OP_LOAD_PROP);
}
return; return;
} }
@ -1895,7 +1925,9 @@ static void parseAssignOrCall(BasParserT *p) {
// Emit: push current form ref, push ctrl name, FIND_CTRL // Emit: push current form ref, push ctrl name, FIND_CTRL
advance(p); // consume DOT advance(p); // consume DOT
if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { // Accept any identifier or keyword as a member name — keywords
// like Load, Show, Hide, Clear are valid method names on controls.
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property or method name"); errorExpected(p, "property or method name");
return; return;
} }
@ -2027,7 +2059,7 @@ static void parseAssignOrCall(BasParserT *p) {
basEmit8(&p->cg, OP_FIND_CTRL_IDX); basEmit8(&p->cg, OP_FIND_CTRL_IDX);
expect(p, TOK_DOT); expect(p, TOK_DOT);
if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "property or method name"); errorExpected(p, "property or method name");
return; return;
} }
@ -5023,7 +5055,7 @@ static void parseStatement(BasParserT *p) {
} }
advance(p); // consume DOT advance(p); // consume DOT
if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
errorExpected(p, "method or member name after Me."); errorExpected(p, "method or member name after Me.");
break; break;
} }

View file

@ -28,6 +28,7 @@ typedef struct {
char error[1024]; char error[1024];
bool hasError; bool hasError;
int32_t errorLine; int32_t errorLine;
int32_t prevLine; // line of the previous token (for error reporting)
int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName
int32_t optionBase; // default array lower bound (0 or 1) int32_t optionBase; // default array lower bound (0 or 1)
bool optionCompareText; // true = case-insensitive string comparison bool optionCompareText; // true = case-insensitive string comparison

View file

@ -118,19 +118,19 @@ BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) {
} }
} }
// Search form scope (active form-scoped symbols shadow globals) // Search form scope and global scope
for (int32_t i = tab->count - 1; i >= 0; i--) { for (int32_t i = tab->count - 1; i >= 0; i--) {
if (tab->symbols[i].formScopeEnded) { if (tab->symbols[i].formScopeEnded) {
continue; continue;
} }
if (tab->symbols[i].scope == SCOPE_FORM && namesEqual(tab->symbols[i].name, name)) { if ((tab->symbols[i].scope == SCOPE_FORM || tab->symbols[i].scope == SCOPE_GLOBAL) &&
namesEqual(tab->symbols[i].name, name)) {
return &tab->symbols[i]; return &tab->symbols[i];
} }
} }
// Search global scope return NULL;
return basSymTabFindGlobal(tab, name);
} }

File diff suppressed because it is too large Load diff

View file

@ -33,8 +33,9 @@ typedef struct BasControlT BasControlT;
// ============================================================ // ============================================================
typedef struct { typedef struct {
int32_t id; int32_t id;
char name[BAS_MAX_CTRL_NAME]; char name[BAS_MAX_CTRL_NAME];
BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored)
} BasMenuIdMapT; } BasMenuIdMapT;
// ============================================================ // ============================================================
@ -53,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
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
} BasControlT; } BasControlT;
// ============================================================ // ============================================================
@ -65,7 +67,7 @@ typedef struct BasFormT {
WidgetT *root; // widget root (from wgtInitWindow) WidgetT *root; // widget root (from wgtInitWindow)
WidgetT *contentBox; // VBox/HBox for user controls WidgetT *contentBox; // VBox/HBox for user controls
AppContextT *ctx; // DVX app context AppContextT *ctx; // DVX app context
BasControlT *controls; // stb_ds dynamic array BasControlT **controls; // stb_ds array of heap-allocated pointers
int32_t controlCount; int32_t controlCount;
BasVmT *vm; // VM for event dispatch BasVmT *vm; // VM for event dispatch
BasModuleT *module; // compiled module (for SUB lookup) BasModuleT *module; // compiled module (for SUB lookup)
@ -78,13 +80,16 @@ typedef struct BasFormT {
bool frmHasResizable; // true if Resizable was explicitly set bool frmHasResizable; // true if Resizable was explicitly set
bool frmCentered; bool frmCentered;
bool frmAutoSize; bool frmAutoSize;
bool frmHBox; // true if Layout = "HBox" char frmLayout[32]; // "VBox", "HBox", or "WrapBox"
// 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;
// Menu ID to name mapping (for event dispatch) // Menu ID to name mapping (for event dispatch)
BasMenuIdMapT *menuIdMap; BasMenuIdMapT *menuIdMap;
int32_t menuIdMapCount; int32_t menuIdMapCount;
// Synthetic control entry for the form itself, so that
// FormName.Property works through the same getProp/setProp path.
BasControlT formCtrl;
} BasFormT; } BasFormT;
// ============================================================ // ============================================================
@ -102,7 +107,7 @@ typedef struct {
AppContextT *ctx; // DVX app context AppContextT *ctx; // DVX app context
BasVmT *vm; // shared VM instance BasVmT *vm; // shared VM instance
BasModuleT *module; // compiled module BasModuleT *module; // compiled module
BasFormT *forms; // stb_ds dynamic array 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
BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources

View file

@ -115,6 +115,84 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
} }
// ============================================================
// dsgnCreateFormWindow
// ============================================================
WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
// wgtInitWindow creates a VBox root. If the requested layout is VBox
// (or empty/missing), reuse root directly to avoid double-nesting.
if (!layout || !layout[0] || strcasecmp(layout, "VBox") == 0) {
return root;
}
// Look up the layout widget by BASIC name and create it dynamically
const char *wgtName = wgtFindByBasName(layout);
if (wgtName) {
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT) {
const void *api = wgtGetApi(wgtName);
if (api) {
// All WGT_CREATE_PARENT APIs have create(parent) as the first function pointer
WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api;
WidgetT *box = createFn(root);
box->weight = 100;
return box;
}
}
}
// Unknown layout — fall back to root VBox
return root;
}
WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) {
int32_t defW = (width > 0) ? width : 400;
int32_t defH = (height > 0) ? height : 300;
WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable);
if (!win) {
return NULL;
}
win->visible = false;
WidgetT *root = wgtInitWindow(ctx, win);
if (!root) {
dvxDestroyWindow(ctx, win);
return NULL;
}
WidgetT *contentBox = dsgnCreateContentBox(root, layout);
// Apply sizing
if (autoSize) {
dvxFitWindow(ctx, win);
} else if (width > 0 && height > 0) {
dvxResizeWindow(ctx, win, width, height);
}
// Apply positioning
if (centered) {
win->x = (ctx->display.width - win->w) / 2;
win->y = (ctx->display.height - win->h) / 2;
} else if (left > 0 || top > 0) {
win->x = left;
win->y = top;
}
*outRoot = root;
*outContentBox = contentBox;
return win;
}
// ============================================================ // ============================================================
// dsgnBuildPreviewMenuBar // dsgnBuildPreviewMenuBar
// ============================================================ // ============================================================
@ -151,7 +229,15 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
} else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) { } else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption); menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) { } else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, -1); int32_t id = DSGN_MENU_ID_BASE + i;
if (mi->radioCheck) {
wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
} else if (mi->checked) {
wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
} else {
wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
}
} }
} }
} }
@ -180,8 +266,8 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t
int32_t count = ds->form ? (int32_t)arrlen(ds->form->controls) : 0; int32_t count = ds->form ? (int32_t)arrlen(ds->form->controls) : 0;
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
if (strncasecmp(ds->form->controls[i].name, prefix, prefixLen) == 0) { if (strncasecmp(ds->form->controls[i]->name, prefix, prefixLen) == 0) {
int32_t num = atoi(ds->form->controls[i].name + prefixLen); int32_t num = atoi(ds->form->controls[i]->name + prefixLen);
if (num > highest) { if (num > highest) {
highest = num; highest = num;
@ -209,20 +295,36 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
// then parent children inside their containers. // then parent children inside their containers.
// Pass 1: create all widgets as top-level children // Pass 1: create all widgets as top-level children
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = &ds->form->controls[i]; DsgnControlT *ctrl = ds->form->controls[i];
if (ctrl->widget) { if (ctrl->widget) {
continue; continue;
} }
// Find the parent widget // Find the parent widget. For containers with a non-VBox Layout
// property, create a content box inside so children use the
// correct layout direction.
WidgetT *parent = contentBox; WidgetT *parent = contentBox;
if (ctrl->parentName[0]) { if (ctrl->parentName[0]) {
for (int32_t j = 0; j < count; j++) { for (int32_t j = 0; j < count; j++) {
if (j != i && ds->form->controls[j].widget && if (j != i && ds->form->controls[j]->widget &&
strcasecmp(ds->form->controls[j].name, ctrl->parentName) == 0) { strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
parent = ds->form->controls[j].widget; DsgnControlT *pc = ds->form->controls[j];
parent = pc->widget;
const char *layout = getPropValue(pc, "Layout");
if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
// Check if we already created a content box inside
if (!parent->firstChild || !parent->firstChild->userData ||
parent->firstChild->userData != (void *)pc) {
WidgetT *box = dsgnCreateContentBox(parent, layout);
box->userData = (void *)pc;
}
parent = parent->firstChild;
}
break; break;
} }
} }
@ -350,6 +452,9 @@ const char *dsgnDefaultEvent(const char *typeName) {
void dsgnFree(DsgnStateT *ds) { void dsgnFree(DsgnStateT *ds) {
if (ds->form) { if (ds->form) {
for (int32_t i = 0; i < arrlen(ds->form->controls); i++) {
free(ds->form->controls[i]);
}
arrfree(ds->form->controls); arrfree(ds->form->controls);
arrfree(ds->form->menuItems); arrfree(ds->form->menuItems);
free(ds->form->code); free(ds->form->code);
@ -407,6 +512,8 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
bool inForm = false; bool inForm = false;
bool inMenu = false; bool inMenu = false;
int32_t menuNestDepth = 0; int32_t menuNestDepth = 0;
int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level)
bool blockIsContainer[DSGN_MAX_FRM_NESTING]; // whether each block is a container
// Parent name stack for nesting (index 0 = form level) // Parent name stack for nesting (index 0 = form level)
char parentStack[8][DSGN_MAX_NAME]; char parentStack[8][DSGN_MAX_NAME];
@ -507,25 +614,29 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
curCtrl = NULL; // not a control curCtrl = NULL; // not a control
menuNestDepth++; menuNestDepth++;
inMenu = true; inMenu = true;
if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; }
blockDepth++;
} else if (inForm) { } else if (inForm) {
DsgnControlT ctrl; DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
memset(&ctrl, 0, sizeof(ctrl)); cp->index = -1;
ctrl.index = -1; snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName);
snprintf(ctrl.name, DSGN_MAX_NAME, "%s", ctrlName); snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName);
// Set parent from current nesting // Set parent from current nesting
if (nestDepth > 0) { if (nestDepth > 0) {
snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]); snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]);
} }
ctrl.width = DEFAULT_CTRL_W; cp->width = DEFAULT_CTRL_W;
ctrl.height = DEFAULT_CTRL_H; cp->height = DEFAULT_CTRL_H;
arrput(form->controls, ctrl); arrput(form->controls, cp);
curCtrl = &form->controls[arrlen(form->controls) - 1]; curCtrl = form->controls[arrlen(form->controls) - 1];
bool isCtrl = dsgnIsContainer(typeName);
if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; }
blockDepth++;
// If this is a container, push onto parent stack // If this is a container, push onto parent stack
if (dsgnIsContainer(typeName) && nestDepth < 7) { if (isCtrl && nestDepth < 7) {
snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName); snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName);
nestDepth++; nestDepth++;
} }
@ -535,22 +646,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
} }
if (strcasecmp(trimmed, "End") == 0) { if (strcasecmp(trimmed, "End") == 0) {
if (inMenu) { if (blockDepth > 0) {
menuNestDepth--; blockDepth--;
curMenuItem = NULL;
if (menuNestDepth <= 0) { if (inMenu) {
menuNestDepth = 0; menuNestDepth--;
inMenu = false; curMenuItem = NULL;
}
} else if (curCtrl) {
// If we're closing a container, pop the parent stack
if (nestDepth > 0 && strcasecmp(parentStack[nestDepth - 1], curCtrl->name) == 0) {
nestDepth--;
}
curCtrl = NULL; if (menuNestDepth <= 0) {
menuNestDepth = 0;
inMenu = false;
}
} else {
// If we're closing a container, pop the parent stack
if (blockDepth < DSGN_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) {
nestDepth--;
}
curCtrl = NULL;
}
} else { } else {
// blockDepth == 0: this is the form's closing End
inForm = false; inForm = false;
// Everything after the form's closing End is code // Everything after the form's closing End is code
@ -619,9 +735,10 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
val[vi] = '\0'; val[vi] = '\0';
if (curMenuItem) { if (curMenuItem) {
if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); } if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); }
else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
} else if (curCtrl) { } else if (curCtrl) {
if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); } if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); }
else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); } else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); }
@ -693,11 +810,12 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
// Delete key -- remove the selected control and any children // Delete key -- remove the selected control and any children
if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < count) { if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < count) {
const char *delName = ds->form->controls[ds->selectedIdx].name; const char *delName = ds->form->controls[ds->selectedIdx]->name;
// Remove children first (controls whose parentName matches) // Remove children first (controls whose parentName matches)
for (int32_t i = count - 1; i >= 0; i--) { for (int32_t i = count - 1; i >= 0; i--) {
if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i].parentName, delName) == 0) { if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i]->parentName, delName) == 0) {
free(ds->form->controls[i]);
arrdel(ds->form->controls, i); arrdel(ds->form->controls, i);
if (i < ds->selectedIdx) { if (i < ds->selectedIdx) {
@ -706,6 +824,7 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
} }
} }
free(ds->form->controls[ds->selectedIdx]);
arrdel(ds->form->controls, ds->selectedIdx); arrdel(ds->form->controls, ds->selectedIdx);
ds->selectedIdx = -1; ds->selectedIdx = -1;
ds->form->dirty = true; ds->form->dirty = true;
@ -728,7 +847,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
if (drag) { if (drag) {
if (ds->mode == DSGN_RESIZING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { if (ds->mode == DSGN_RESIZING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
int32_t dx = x - ds->dragStartX; int32_t dx = x - ds->dragStartX;
int32_t dy = y - ds->dragStartY; int32_t dy = y - ds->dragStartY;
@ -755,15 +874,15 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
} else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { } else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
// Determine if we should swap with a neighbor based on drag direction // Determine if we should swap with a neighbor based on drag direction
int32_t dy = y - ds->dragStartY; int32_t dy = y - ds->dragStartY;
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
if (dy > 0 && ctrl->widget) { if (dy > 0 && ctrl->widget) {
// Dragging down -- swap with next control if past its midpoint // Dragging down -- swap with next control if past its midpoint
if (ds->selectedIdx < ctrlCount - 1) { if (ds->selectedIdx < ctrlCount - 1) {
DsgnControlT *next = &ds->form->controls[ds->selectedIdx + 1]; DsgnControlT *next = ds->form->controls[ds->selectedIdx + 1];
if (next->widget && y > next->widget->y + next->widget->h / 2) { if (next->widget && y > next->widget->y + next->widget->h / 2) {
DsgnControlT tmp = ds->form->controls[ds->selectedIdx]; DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1]; ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1];
ds->form->controls[ds->selectedIdx + 1] = tmp; ds->form->controls[ds->selectedIdx + 1] = tmp;
rebuildWidgets(ds); rebuildWidgets(ds);
@ -775,10 +894,10 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
} else if (dy < 0 && ctrl->widget) { } else if (dy < 0 && ctrl->widget) {
// Dragging up -- swap with previous control if past its midpoint // Dragging up -- swap with previous control if past its midpoint
if (ds->selectedIdx > 0) { if (ds->selectedIdx > 0) {
DsgnControlT *prev = &ds->form->controls[ds->selectedIdx - 1]; DsgnControlT *prev = ds->form->controls[ds->selectedIdx - 1];
if (prev->widget && y < prev->widget->y + prev->widget->h / 2) { if (prev->widget && y < prev->widget->y + prev->widget->h / 2) {
DsgnControlT tmp = ds->form->controls[ds->selectedIdx]; DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1]; ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1];
ds->form->controls[ds->selectedIdx - 1] = tmp; ds->form->controls[ds->selectedIdx - 1] = tmp;
rebuildWidgets(ds); rebuildWidgets(ds);
@ -803,10 +922,10 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
if (ds->activeTool[0] == '\0') { if (ds->activeTool[0] == '\0') {
// Check grab handles of selected control first // Check grab handles of selected control first
if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
DsgnHandleE handle = hitTestHandles(&ds->form->controls[ds->selectedIdx], x, y); DsgnHandleE handle = hitTestHandles(ds->form->controls[ds->selectedIdx], x, y);
if (handle != HANDLE_NONE) { if (handle != HANDLE_NONE) {
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
ds->mode = DSGN_RESIZING; ds->mode = DSGN_RESIZING;
ds->activeHandle = handle; ds->activeHandle = handle;
ds->dragStartX = x; ds->dragStartX = x;
@ -833,20 +952,19 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
// Non-pointer tool: place a new control // Non-pointer tool: place a new control
const char *typeName = ds->activeTool; const char *typeName = ds->activeTool;
DsgnControlT ctrl; DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
memset(&ctrl, 0, sizeof(ctrl)); cp->index = -1;
ctrl.index = -1; dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME);
dsgnAutoName(ds, typeName, ctrl.name, DSGN_MAX_NAME); snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); cp->width = DEFAULT_CTRL_W;
ctrl.width = DEFAULT_CTRL_W; cp->height = DEFAULT_CTRL_H;
ctrl.height = DEFAULT_CTRL_H; setPropValue(cp, "Caption", cp->name);
setPropValue(&ctrl, "Caption", ctrl.name);
// Determine parent: if click is inside a container, nest there // Determine parent: if click is inside a container, nest there
WidgetT *parentWidget = ds->form->contentBox; WidgetT *parentWidget = ds->form->contentBox;
for (int32_t i = ctrlCount - 1; i >= 0; i--) { for (int32_t i = ctrlCount - 1; i >= 0; i--) {
DsgnControlT *pc = &ds->form->controls[i]; DsgnControlT *pc = ds->form->controls[i];
if (pc->widget && dsgnIsContainer(pc->typeName)) { if (pc->widget && dsgnIsContainer(pc->typeName)) {
int32_t wx = pc->widget->x; int32_t wx = pc->widget->x;
@ -855,7 +973,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
int32_t wh = pc->widget->h; int32_t wh = pc->widget->h;
if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) { if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", pc->name); snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name);
parentWidget = pc->widget; parentWidget = pc->widget;
break; break;
} }
@ -864,19 +982,19 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
// Create the live widget // Create the live widget
if (parentWidget) { if (parentWidget) {
ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget); cp->widget = dsgnCreateDesignWidget(typeName, parentWidget);
if (ctrl.widget) { if (cp->widget) {
ctrl.widget->minW = wgtPixels(ctrl.width); cp->widget->minW = wgtPixels(cp->width);
ctrl.widget->minH = wgtPixels(ctrl.height); cp->widget->minH = wgtPixels(cp->height);
} }
} }
arrput(ds->form->controls, ctrl); arrput(ds->form->controls, cp);
ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1; ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1;
// Set text AFTER arrput so pointers into the array element are stable // Set text AFTER arrput so pointers are stable (heap-allocated, so always stable)
DsgnControlT *stable = &ds->form->controls[ds->selectedIdx]; DsgnControlT *stable = ds->form->controls[ds->selectedIdx];
if (stable->widget) { if (stable->widget) {
const char *caption = getPropValue(stable, "Caption"); const char *caption = getPropValue(stable, "Caption");
@ -931,7 +1049,7 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
return; return;
} }
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
return; return;
@ -980,7 +1098,7 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
int32_t count = (int32_t)arrlen(form->controls); int32_t count = (int32_t)arrlen(form->controls);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
const DsgnControlT *ctrl = &form->controls[i]; const DsgnControlT *ctrl = form->controls[i];
// Only output controls whose parent matches // Only output controls whose parent matches
if (parentName[0] == '\0' && ctrl->parentName[0] != '\0') { continue; } if (parentName[0] == '\0' && ctrl->parentName[0] != '\0') { continue; }
@ -1160,6 +1278,14 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n"); pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n");
} }
if (mi->radioCheck) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' ';
}
pos += snprintf(buf + pos, bufSize - pos, "RadioCheck = True\n");
}
if (!mi->enabled) { if (!mi->enabled) {
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) { for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
buf[pos++] = ' '; buf[pos++] = ' ';
@ -1226,7 +1352,7 @@ const char *dsgnSelectedName(const DsgnStateT *ds) {
} }
if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
return ds->form->controls[ds->selectedIdx].name; return ds->form->controls[ds->selectedIdx]->name;
} }
return ds->form->name; return ds->form->name;
@ -1260,7 +1386,7 @@ static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) {
int32_t count = (int32_t)arrlen(ds->form->controls); int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = count - 1; i >= 0; i--) { for (int32_t i = count - 1; i >= 0; i--) {
const DsgnControlT *ctrl = &ds->form->controls[i]; const DsgnControlT *ctrl = ds->form->controls[i];
if (!ctrl->widget || !ctrl->widget->visible) { if (!ctrl->widget || !ctrl->widget->visible) {
continue; continue;
@ -1375,7 +1501,7 @@ static void rebuildWidgets(DsgnStateT *ds) {
int32_t count = (int32_t)arrlen(ds->form->controls); int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
ds->form->controls[i].widget = NULL; ds->form->controls[i]->widget = NULL;
} }
// Recreate all widgets in current array order // Recreate all widgets in current array order

View file

@ -20,11 +20,13 @@
// Limits // Limits
// ============================================================ // ============================================================
#define DSGN_MAX_NAME 32 #define DSGN_MAX_NAME 32
#define DSGN_MAX_TEXT 256 #define DSGN_MAX_TEXT 256
#define DSGN_MAX_PROPS 32 #define DSGN_MAX_PROPS 32
#define DSGN_GRID_SIZE 8 #define DSGN_MAX_FRM_NESTING 16
#define DSGN_HANDLE_SIZE 6 #define DSGN_GRID_SIZE 8
#define DSGN_HANDLE_SIZE 6
#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items
// ============================================================ // ============================================================
// Design-time property (stored as key=value strings) // Design-time property (stored as key=value strings)
@ -44,6 +46,7 @@ typedef struct {
char name[DSGN_MAX_NAME]; // "mnuFile" char name[DSGN_MAX_NAME]; // "mnuFile"
int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu
bool checked; bool checked;
bool radioCheck; // true = radio bullet instead of checkmark
bool enabled; // default true bool enabled; // default true
} DsgnMenuItemT; } DsgnMenuItemT;
@ -83,7 +86,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
DsgnControlT *controls; // stb_ds dynamic array 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;
WidgetT *contentBox; // VBox parent for live widgets WidgetT *contentBox; // VBox parent for live widgets
@ -198,6 +201,18 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent);
// Used in the form designer to preview the menu layout. // Used in the form designer to preview the menu layout.
void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form); void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form);
// Create a layout container (VBox/HBox/WrapBox) from a layout name string.
// For VBox, reuses the root directly. For HBox/WrapBox, creates a child.
WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout);
// Create and configure a window from form properties.
// Shared by the designer and runtime to ensure consistent behavior.
// Creates the window, widget root, and layout container (VBox/HBox/WrapBox).
// Applies sizing (autoSize or explicit) and positioning (centered or explicit).
// On success, sets *outRoot and *outContentBox and returns the window.
// On failure, returns NULL.
WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox);
// ============================================================ // ============================================================
// Code rename support (implemented in ideMain.c) // Code rename support (implemented in ideMain.c)
// ============================================================ // ============================================================

File diff suppressed because it is too large Load diff

View file

@ -47,6 +47,7 @@ typedef struct {
WidgetT *captionInput; WidgetT *captionInput;
WidgetT *nameInput; WidgetT *nameInput;
WidgetT *checkedCb; WidgetT *checkedCb;
WidgetT *radioCheckCb;
WidgetT *enabledCb; WidgetT *enabledCb;
WidgetT *listBox; WidgetT *listBox;
} MnuEdStateT; } MnuEdStateT;
@ -143,8 +144,9 @@ static void applyFields(void) {
sMed.nameAutoGen = true; sMed.nameAutoGen = true;
} }
mi->checked = wgtCheckboxIsChecked(sMed.checkedCb); mi->checked = wgtCheckboxIsChecked(sMed.checkedCb);
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb); mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb);
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
} }
@ -180,8 +182,9 @@ static void loadFields(void) {
wgtSetText(sMed.captionInput, ""); wgtSetText(sMed.captionInput, "");
wgtSetText(sMed.nameInput, ""); wgtSetText(sMed.nameInput, "");
wgtCheckboxSetChecked(sMed.checkedCb, false); wgtCheckboxSetChecked(sMed.checkedCb, false);
wgtCheckboxSetChecked(sMed.radioCheckCb, false);
wgtCheckboxSetChecked(sMed.enabledCb, true); wgtCheckboxSetChecked(sMed.enabledCb, true);
sMed.nameAutoGen = true; // new blank item auto-gen eligible sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
return; return;
} }
@ -190,6 +193,7 @@ static void loadFields(void) {
wgtSetText(sMed.captionInput, mi->caption); wgtSetText(sMed.captionInput, mi->caption);
wgtSetText(sMed.nameInput, mi->name); wgtSetText(sMed.nameInput, mi->name);
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked); wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled); wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
} }
@ -661,14 +665,15 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
// Check row // Check row
WidgetT *chkRow = wgtHBox(root); WidgetT *chkRow = wgtHBox(root);
chkRow->spacing = wgtPixels(12); chkRow->spacing = wgtPixels(12);
sMed.checkedCb = wgtCheckbox(chkRow, "Checked"); sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled"); sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
wgtCheckboxSetChecked(sMed.enabledCb, true); wgtCheckboxSetChecked(sMed.enabledCb, true);
// Listbox // Listbox
sMed.listBox = wgtListBox(root); sMed.listBox = wgtListBox(root);
sMed.listBox->weight = 100; sMed.listBox->weight = 100;
sMed.listBox->onClick = onListClick; sMed.listBox->onChange = onListClick;
// Arrow buttons // Arrow buttons
WidgetT *arrowRow = wgtHBox(root); WidgetT *arrowRow = wgtHBox(root);

View file

@ -82,7 +82,7 @@ static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], in
static void onPrpClose(WindowT *win); static void onPrpClose(WindowT *win);
static void onPropDblClick(WidgetT *w); static void onPropDblClick(WidgetT *w);
static void onTreeItemClick(WidgetT *w); static void onTreeItemClick(WidgetT *w);
static void onTreeReorder(WidgetT *w); static void onTreeChange(WidgetT *w);
// ============================================================ // ============================================================
@ -134,7 +134,7 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam
int32_t ctrlCount = (int32_t)arrlen(ds->form->controls); int32_t ctrlCount = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < ctrlCount; i++) { for (int32_t i = 0; i < ctrlCount; i++) {
DsgnControlT *ctrl = &ds->form->controls[i]; DsgnControlT *ctrl = ds->form->controls[i];
if (strcasecmp(ctrl->typeName, "Data") != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) { if (strcasecmp(ctrl->typeName, "Data") != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) {
continue; continue;
@ -288,6 +288,7 @@ static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], in
#define PROP_TYPE_BOOL WGT_IFACE_BOOL #define PROP_TYPE_BOOL WGT_IFACE_BOOL
#define PROP_TYPE_ENUM WGT_IFACE_ENUM #define PROP_TYPE_ENUM WGT_IFACE_ENUM
#define PROP_TYPE_READONLY 255 #define PROP_TYPE_READONLY 255
#define PROP_TYPE_LAYOUT 251
#define PROP_TYPE_DATASOURCE 254 #define PROP_TYPE_DATASOURCE 254
#define PROP_TYPE_DATAFIELD 253 #define PROP_TYPE_DATAFIELD 253
#define PROP_TYPE_RECORDSRC 252 #define PROP_TYPE_RECORDSRC 252
@ -317,6 +318,7 @@ static uint8_t getPropType(const char *propName, const char *typeName) {
if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; }
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, "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; }
@ -384,7 +386,7 @@ static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visib
int32_t count = (int32_t)arrlen(ds->form->controls); int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
DsgnControlT *child = &ds->form->controls[i]; DsgnControlT *child = ds->form->controls[i];
if (strcasecmp(child->parentName, parentName) != 0) { if (strcasecmp(child->parentName, parentName) != 0) {
continue; continue;
@ -421,40 +423,78 @@ static void onPropDblClick(WidgetT *w) {
const char *propName = sCellData[row * 2]; const char *propName = sCellData[row * 2];
const char *curValue = sCellData[row * 2 + 1]; const char *curValue = sCellData[row * 2 + 1];
// Layout toggles directly -- no input box needed // Layout — select from discovered layout containers
if (strcasecmp(propName, "Layout") == 0 && sDs->selectedIdx < 0) { if (strcasecmp(propName, "Layout") == 0) {
if (strcasecmp(sDs->form->layout, "VBox") == 0) { // Discover available layout types from loaded widget interfaces.
snprintf(sDs->form->layout, DSGN_MAX_NAME, "HBox"); // A layout container is isContainer with WGT_CREATE_PARENT (no extra args).
} else { const char *layoutNames[32];
snprintf(sDs->form->layout, DSGN_MAX_NAME, "VBox"); int32_t layoutCount = 0;
int32_t ifaceTotal = wgtIfaceCount();
for (int32_t i = 0; i < ifaceTotal && layoutCount < 32; i++) {
const WgtIfaceT *iface = wgtIfaceAt(i, NULL);
if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT && iface->basName) {
layoutNames[layoutCount++] = iface->basName;
}
} }
if (layoutCount == 0) {
return;
}
// Determine whose layout we're changing
char *layoutField = NULL;
if (sDs->selectedIdx < 0) {
layoutField = sDs->form->layout;
} else {
DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
for (int32_t pi = 0; pi < ctrl->propCount; pi++) {
if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) {
layoutField = ctrl->props[pi].value;
break;
}
}
if (!layoutField) {
return;
}
}
// Find current selection
int32_t defIdx = 0;
for (int32_t i = 0; i < layoutCount; i++) {
if (strcasecmp(layoutField, layoutNames[i]) == 0) {
defIdx = i;
break;
}
}
int32_t chosenIdx = 0;
if (!dvxChoiceDialog(sPrpCtx, "Layout", "Select layout type:", layoutNames, layoutCount, defIdx, &chosenIdx)) {
return;
}
snprintf(layoutField, DSGN_MAX_NAME, "%s", layoutNames[chosenIdx]);
sDs->form->dirty = true; sDs->form->dirty = true;
// Replace the content box with the new layout type // Rebuild the form designer to apply the new layout
if (sDs->formWin && sDs->formWin->widgetRoot) { if (sDs->formWin && sDs->formWin->widgetRoot) {
WidgetT *root = sDs->formWin->widgetRoot; WidgetT *root = sDs->formWin->widgetRoot;
// Remove old content box
root->firstChild = NULL; root->firstChild = NULL;
root->lastChild = NULL; root->lastChild = NULL;
// Create new content box WidgetT *contentBox = dsgnCreateContentBox(root, layoutField);
WidgetT *contentBox;
if (strcasecmp(sDs->form->layout, "HBox") == 0) {
contentBox = wgtHBox(root);
} else {
contentBox = wgtVBox(root);
}
contentBox->weight = 100;
// Clear widget pointers and recreate
int32_t cc = (int32_t)arrlen(sDs->form->controls); int32_t cc = (int32_t)arrlen(sDs->form->controls);
for (int32_t ci = 0; ci < cc; ci++) { for (int32_t ci = 0; ci < cc; ci++) {
sDs->form->controls[ci].widget = NULL; sDs->form->controls[ci]->widget = NULL;
} }
dsgnCreateWidgets(sDs, contentBox); dsgnCreateWidgets(sDs, contentBox);
@ -473,7 +513,7 @@ static void onPropDblClick(WidgetT *w) {
int32_t ctrlCount = (int32_t)arrlen(sDs->form->controls); int32_t ctrlCount = (int32_t)arrlen(sDs->form->controls);
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < ctrlCount) { if (sDs->selectedIdx >= 0 && sDs->selectedIdx < ctrlCount) {
ctrlTypeName = sDs->form->controls[sDs->selectedIdx].typeName; ctrlTypeName = sDs->form->controls[sDs->selectedIdx]->typeName;
} }
uint8_t propType = getPropType(propName, ctrlTypeName); uint8_t propType = getPropType(propName, ctrlTypeName);
@ -523,8 +563,8 @@ static void onPropDblClick(WidgetT *w) {
dataNames[dataCount++] = "(none)"; dataNames[dataCount++] = "(none)";
for (int32_t i = 0; i < formCtrlCount && dataCount < 16; i++) { for (int32_t i = 0; i < formCtrlCount && dataCount < 16; i++) {
if (strcasecmp(sDs->form->controls[i].typeName, "Data") == 0) { if (strcasecmp(sDs->form->controls[i]->typeName, "Data") == 0) {
dataNames[dataCount++] = sDs->form->controls[i].name; dataNames[dataCount++] = sDs->form->controls[i]->name;
} }
} }
@ -561,7 +601,7 @@ static void onPropDblClick(WidgetT *w) {
const char *dataSrc = ""; const char *dataSrc = "";
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
DsgnControlT *selCtrl = &sDs->form->controls[sDs->selectedIdx]; DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
if (strcasecmp(selCtrl->typeName, "Data") == 0) { if (strcasecmp(selCtrl->typeName, "Data") == 0) {
dataSrc = selCtrl->name; dataSrc = selCtrl->name;
@ -623,7 +663,7 @@ static void onPropDblClick(WidgetT *w) {
const char *dbName = ""; const char *dbName = "";
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) {
DsgnControlT *selCtrl = &sDs->form->controls[sDs->selectedIdx]; DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx];
for (int32_t i = 0; i < selCtrl->propCount; i++) { for (int32_t i = 0; i < selCtrl->propCount; i++) {
if (strcasecmp(selCtrl->props[i].name, "DatabaseName") == 0) { if (strcasecmp(selCtrl->props[i].name, "DatabaseName") == 0) {
@ -699,7 +739,7 @@ static void onPropDblClick(WidgetT *w) {
int32_t count = (int32_t)arrlen(sDs->form->controls); int32_t count = (int32_t)arrlen(sDs->form->controls);
if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) { if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) {
DsgnControlT *ctrl = &sDs->form->controls[sDs->selectedIdx]; DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx];
if (strcasecmp(propName, "Name") == 0) { if (strcasecmp(propName, "Name") == 0) {
char oldName[DSGN_MAX_NAME]; char oldName[DSGN_MAX_NAME];
@ -707,7 +747,7 @@ static void onPropDblClick(WidgetT *w) {
// Rename all members of a control array, not just the selected one // Rename all members of a control array, not just the selected one
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = &sDs->form->controls[i]; DsgnControlT *c = sDs->form->controls[i];
if (strcasecmp(c->name, oldName) == 0) { if (strcasecmp(c->name, oldName) == 0) {
snprintf(c->name, DSGN_MAX_NAME, "%.31s", newValue); snprintf(c->name, DSGN_MAX_NAME, "%.31s", newValue);
@ -722,7 +762,7 @@ static void onPropDblClick(WidgetT *w) {
// references on all other controls that pointed to the old name // references on all other controls that pointed to the old name
if (strcasecmp(ctrl->typeName, "Data") == 0) { if (strcasecmp(ctrl->typeName, "Data") == 0) {
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
DsgnControlT *c = &sDs->form->controls[i]; DsgnControlT *c = sDs->form->controls[i];
for (int32_t j = 0; j < c->propCount; j++) { for (int32_t j = 0; j < c->propCount; j++) {
if ((strcasecmp(c->props[j].name, "DataSource") == 0 || if ((strcasecmp(c->props[j].name, "DataSource") == 0 ||
@ -1002,7 +1042,7 @@ static void onTreeItemClick(WidgetT *w) {
int32_t count = (int32_t)arrlen(sDs->form->controls); int32_t count = (int32_t)arrlen(sDs->form->controls);
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
if (strcmp(sDs->form->controls[i].name, clickedName) == 0) { if (strcmp(sDs->form->controls[i]->name, clickedName) == 0) {
sDs->selectedIdx = i; sDs->selectedIdx = i;
if (sDs->formWin) { if (sDs->formWin) {
@ -1022,7 +1062,7 @@ static void onTreeItemClick(WidgetT *w) {
// Walk tree items recursively, collecting control names in order. // Walk tree items recursively, collecting control names in order.
static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcCount, DsgnControlT **outArr, const char *parentName) { static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData; const char *label = (const char *)item->userData;
@ -1041,10 +1081,9 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcC
itemName[ni] = '\0'; itemName[ni] = '\0';
for (int32_t i = 0; i < srcCount; i++) { for (int32_t i = 0; i < srcCount; i++) {
if (strcmp(srcArr[i].name, itemName) == 0) { if (strcmp(srcArr[i]->name, itemName) == 0) {
DsgnControlT ctrl = srcArr[i]; snprintf(srcArr[i]->parentName, DSGN_MAX_NAME, "%s", parentName);
snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentName); arrput(*outArr, srcArr[i]);
arrput(*outArr, ctrl);
// Recurse into children (for containers) // Recurse into children (for containers)
if (item->firstChild) { if (item->firstChild) {
@ -1058,23 +1097,60 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcC
} }
static void onTreeReorder(WidgetT *w) { // Check whether the tree order matches the controls array.
// Returns true if they match (no reorder happened).
static bool treeOrderMatches(void) {
WidgetT *formItem = sTree->firstChild;
if (!formItem) {
return true;
}
DsgnControlT **newArr = NULL;
int32_t count = (int32_t)arrlen(sDs->form->controls);
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
bool match = ((int32_t)arrlen(newArr) == count);
if (match) {
for (int32_t i = 0; i < count; i++) {
if (strcmp(newArr[i]->name, sDs->form->controls[i]->name) != 0 ||
strcmp(newArr[i]->parentName, sDs->form->controls[i]->parentName) != 0) {
match = false;
break;
}
}
}
arrfree(newArr);
return match;
}
static void onTreeChange(WidgetT *w) {
(void)w; (void)w;
if (!sDs || !sDs->form || !sTree || sUpdating) { if (!sDs || !sDs->form || !sTree || sUpdating) {
return; return;
} }
// If the order hasn't changed, this is just a selection or expand/collapse.
// The onClick handler on individual items handles selection updates.
if (treeOrderMatches()) {
return;
}
// Actual reorder happened — rebuild the controls array from tree order.
int32_t count = (int32_t)arrlen(sDs->form->controls); int32_t count = (int32_t)arrlen(sDs->form->controls);
DsgnControlT *newArr = NULL; DsgnControlT **newArr = NULL;
WidgetT *formItem = sTree->firstChild; WidgetT *formItem = sTree->firstChild;
if (!formItem) { if (!formItem) {
return; return;
} }
// Collect all controls from the tree in their new order,
// handling nesting (items dragged into containers get parentName updated).
collectTreeOrder(formItem, sDs->form->controls, count, &newArr, ""); collectTreeOrder(formItem, sDs->form->controls, count, &newArr, "");
// If we lost items (dragged above form), revert // If we lost items (dragged above form), revert
@ -1095,7 +1171,7 @@ static void onTreeReorder(WidgetT *w) {
int32_t newCount = (int32_t)arrlen(sDs->form->controls); int32_t newCount = (int32_t)arrlen(sDs->form->controls);
for (int32_t i = 0; i < newCount; i++) { for (int32_t i = 0; i < newCount; i++) {
sDs->form->controls[i].widget = NULL; sDs->form->controls[i]->widget = NULL;
} }
dsgnCreateWidgets(sDs, sDs->form->contentBox); dsgnCreateWidgets(sDs, sDs->form->contentBox);
@ -1138,7 +1214,7 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
// Control tree (top pane) // Control tree (top pane)
sTree = wgtTreeView(splitter); sTree = wgtTreeView(splitter);
sTree->onChange = onTreeReorder; sTree->onChange = onTreeChange;
wgtTreeViewSetReorderable(sTree, true); wgtTreeViewSetReorderable(sTree, true);
// Property ListView (bottom pane) // Property ListView (bottom pane)
@ -1217,7 +1293,7 @@ void prpRebuildTree(DsgnStateT *ds) {
WidgetT **treeItems = NULL; WidgetT **treeItems = NULL;
for (int32_t i = 0; i < count; i++) { for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = &ds->form->controls[i]; DsgnControlT *ctrl = ds->form->controls[i];
char buf[128]; char buf[128];
if (ctrl->index >= 0) { if (ctrl->index >= 0) {
@ -1234,7 +1310,7 @@ void prpRebuildTree(DsgnStateT *ds) {
if (ctrl->parentName[0]) { if (ctrl->parentName[0]) {
for (int32_t j = 0; j < i; j++) { for (int32_t j = 0; j < i; j++) {
if (strcasecmp(ds->form->controls[j].name, ctrl->parentName) == 0 && treeItems) { if (strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0 && treeItems) {
treeParent = treeItems[j]; treeParent = treeItems[j];
break; break;
} }
@ -1265,13 +1341,62 @@ void prpRebuildTree(DsgnStateT *ds) {
// prpRefresh // prpRefresh
// ============================================================ // ============================================================
// Walk tree items recursively to find the one matching a control name.
static WidgetT *findTreeItemByName(WidgetT *parent, const char *name) {
for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) {
const char *label = (const char *)item->userData;
if (label) {
// Labels are "Name (Type)" — match the name portion
int32_t len = 0;
while (label[len] && label[len] != ' ') {
len++;
}
if ((int32_t)strlen(name) == len && strncmp(label, name, len) == 0) {
return item;
}
}
// Recurse into children (containers)
WidgetT *found = findTreeItemByName(item, name);
if (found) {
return found;
}
}
return NULL;
}
void prpRefresh(DsgnStateT *ds) { void prpRefresh(DsgnStateT *ds) {
if (!ds || !ds->form) { if (!ds || !ds->form) {
return; return;
} }
// Don't rebuild the tree here -- just update selection on existing items. // Sync tree selection to match selectedIdx
// prpRebuildTree destroys all items which loses TreeView selection state. if (sTree && !sUpdating) {
WidgetT *formItem = sTree->firstChild;
if (formItem) {
WidgetT *target = NULL;
if (ds->selectedIdx < 0) {
target = formItem;
} else if (ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
target = findTreeItemByName(formItem, ds->form->controls[ds->selectedIdx]->name);
}
if (target) {
sUpdating = true;
wgtTreeViewSetSelected(sTree, target);
sUpdating = false;
}
}
}
// Update property ListView // Update property ListView
if (!sPropList) { if (!sPropList) {
@ -1283,7 +1408,7 @@ void prpRefresh(DsgnStateT *ds) {
int32_t count = (int32_t)arrlen(ds->form->controls); int32_t count = (int32_t)arrlen(ds->form->controls);
if (ds->selectedIdx >= 0 && ds->selectedIdx < count) { if (ds->selectedIdx >= 0 && ds->selectedIdx < count) {
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
char buf[32]; char buf[32];
addPropRow("Name", ctrl->name); addPropRow("Name", ctrl->name);

View file

@ -429,8 +429,12 @@ static void scanAppsDirRecurse(const char *dirPath) {
continue; continue;
} }
// Copy d_name before recursion — readdir may use a shared buffer
char name[MAX_PATH_LEN];
snprintf(name, sizeof(name), "%s", ent->d_name);
char fullPath[MAX_PATH_LEN]; char fullPath[MAX_PATH_LEN];
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name); snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
// Check if this is a directory -- recurse into it // Check if this is a directory -- recurse into it
struct stat st; struct stat st;
@ -440,21 +444,21 @@ static void scanAppsDirRecurse(const char *dirPath) {
continue; continue;
} }
int32_t len = strlen(ent->d_name); int32_t len = strlen(name);
if (len < 5) { if (len < 5) {
continue; continue;
} }
// Check for .app extension (case-insensitive) // Check for .app extension (case-insensitive)
const char *ext = ent->d_name + len - 4; const char *ext = name + len - 4;
if (strcasecmp(ext, ".app") != 0) { if (strcasecmp(ext, ".app") != 0) {
continue; continue;
} }
// Skip ourselves // Skip ourselves
if (strcasecmp(ent->d_name, "progman.app") == 0) { if (strcasecmp(name, "progman.app") == 0) {
continue; continue;
} }
@ -469,7 +473,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
nameLen = SHELL_APP_NAME_MAX - 1; nameLen = SHELL_APP_NAME_MAX - 1;
} }
memcpy(newEntry.name, ent->d_name, nameLen); memcpy(newEntry.name, name, 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') {
@ -480,13 +484,13 @@ 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", ent->d_name); dvxLog("Progman: no icon32 resource in %s", name);
} }
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, ent->d_name, newEntry.iconData ? "yes" : "no"); dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, name, newEntry.iconData ? "yes" : "no");
arrput(sAppFiles, newEntry); arrput(sAppFiles, newEntry);
sAppCount = (int32_t)arrlen(sAppFiles); sAppCount = (int32_t)arrlen(sAppFiles);

View file

@ -23,7 +23,7 @@ TARGETDIR = $(LIBSDIR)/kpunch/libdvx
TARGET = $(TARGETDIR)/libdvx.lib TARGET = $(TARGETDIR)/libdvx.lib
# libdvx.lib export prefixes # libdvx.lib export prefixes
DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _text \ 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 _setClip -E _resetClip -E _stbi_ -E _stbi_write -E _dirtyList \
-E _widget \ -E _widget \
-E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \ -E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \

View file

@ -5069,8 +5069,7 @@ bool dvxUpdate(AppContextT *ctx) {
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->needsPaint && win->onPaint) { if (win->fullRepaint && win->onPaint) {
win->needsPaint = false;
dvxInvalidateWindow(ctx, win); dvxInvalidateWindow(ctx, win);
} }
} }

View file

@ -512,7 +512,7 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
widgetLayoutChildren(root, &ctx->font); widgetLayoutChildren(root, &ctx->font);
// Paint widgets // Paint widgets
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, true);
} }
} }

View file

@ -507,7 +507,7 @@ typedef struct WindowT {
bool resizable; bool resizable;
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 needsPaint; // true until first onPaint call (auto-paint on next frame) bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones
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

@ -85,6 +85,32 @@ uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) {
} }
// ============================================================
// unpackColor
// ============================================================
void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b) {
if (d->format.bitsPerPixel == 8) {
if (color < 256 && d->palette) {
*r = d->palette[color * 3];
*g = d->palette[color * 3 + 1];
*b = d->palette[color * 3 + 2];
} else {
*r = *g = *b = 0;
}
return;
}
uint32_t rv = (color >> d->format.redShift) & ((1u << d->format.redBits) - 1);
uint32_t gv = (color >> d->format.greenShift) & ((1u << d->format.greenBits) - 1);
uint32_t bv = (color >> d->format.blueShift) & ((1u << d->format.blueBits) - 1);
*r = (uint8_t)(rv << (8 - d->format.redBits));
*g = (uint8_t)(gv << (8 - d->format.greenBits));
*b = (uint8_t)(bv << (8 - d->format.blueBits));
}
// ============================================================ // ============================================================
// resetClipRect // resetClipRect
// ============================================================ // ============================================================

View file

@ -37,6 +37,9 @@ void videoShutdown(DisplayT *d);
// linear scan of the grey ramp and chrome entries). // linear scan of the grey ramp and chrome entries).
uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b); uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b);
// Unpack a native pixel value back to 8-bit R, G, B components.
void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b);
// Set the clip rectangle on the display. All subsequent draw operations // Set the clip rectangle on the display. All subsequent draw operations
// will be clipped to this rectangle. The caller is responsible for // will be clipped to this rectangle. The caller is responsible for
// saving and restoring the clip rect around scoped operations. // saving and restoring the clip rect around scoped operations.

View file

@ -248,8 +248,15 @@ typedef struct WidgetT {
bool enabled; bool enabled;
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)
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
// so callbacks receive content-relative coords (e.g. canvas subtracts
// its border so (0,0) = first drawable pixel, matching VB3 behavior).
int32_t contentOffX;
int32_t contentOffY;
// User data and callbacks. These fire for ALL widget types from the // User data and callbacks. These fire for ALL widget types from the
// central event dispatcher, not from individual widget handlers. // central event dispatcher, not from individual widget handlers.
// Type-specific handlers (e.g. button press animation, listbox // Type-specific handlers (e.g. button press animation, listbox
@ -524,7 +531,7 @@ void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT
// clip rect is set to its bounds before calling its paint function. // clip rect is set to its bounds before calling its paint function.
// Overlays (dropdown popups, tooltips) are painted in a second pass // Overlays (dropdown popups, tooltips) are painted in a second pass
// after the main tree so they render on top of everything. // after the main tree so they render on top of everything.
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, bool fullRepaint);
// ============================================================ // ============================================================
// Widget API registry // Widget API registry
@ -567,7 +574,14 @@ const void *wgtGetApi(const char *name);
#define WGT_SIG_INT_BOOL 5 // void fn(WidgetT *, int32_t, bool) #define WGT_SIG_INT_BOOL 5 // void fn(WidgetT *, int32_t, bool)
#define WGT_SIG_RET_INT 6 // int32_t fn(const WidgetT *) #define WGT_SIG_RET_INT 6 // int32_t fn(const WidgetT *)
#define WGT_SIG_RET_BOOL 7 // bool fn(const WidgetT *) #define WGT_SIG_RET_BOOL 7 // bool fn(const WidgetT *)
#define WGT_SIG_RET_BOOL_INT 8 // bool fn(const WidgetT *, int32_t) #define WGT_SIG_RET_BOOL_INT 8 // bool fn(const WidgetT *, int32_t)
#define WGT_SIG_INT3 9 // void fn(WidgetT *, int32_t, int32_t, int32_t)
#define WGT_SIG_INT4 10 // void fn(WidgetT *, int32_t, int32_t, int32_t, int32_t)
#define WGT_SIG_RET_INT_INT_INT 11 // int32_t fn(const WidgetT *, int32_t, int32_t)
#define WGT_SIG_INT_INT_STR 12 // void fn(WidgetT *, int32_t, int32_t, const char *)
#define WGT_SIG_INT5 13 // void fn(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t)
#define WGT_SIG_RET_STR_INT 14 // const char *fn(const WidgetT *, int32_t)
#define WGT_SIG_RET_STR_INT_INT 15 // const char *fn(const WidgetT *, int32_t, int32_t)
// Property descriptor // Property descriptor
typedef struct { typedef struct {

View file

@ -1047,6 +1047,38 @@ void wmAddMenuSeparator(MenuT *menu) {
} }
// ============================================================
// wmRemoveMenuItem -- remove a menu item by command ID
// ============================================================
bool wmRemoveMenuItem(MenuT *menu, int32_t id) {
if (!menu) {
return false;
}
for (int32_t i = 0; i < menu->itemCount; i++) {
if (menu->items[i].id == id && !menu->items[i].separator) {
memmove(&menu->items[i], &menu->items[i + 1], (menu->itemCount - i - 1) * sizeof(MenuItemT));
menu->itemCount--;
return true;
}
}
return false;
}
// ============================================================
// wmClearMenuItems -- remove all items from a menu
// ============================================================
void wmClearMenuItems(MenuT *menu) {
if (menu) {
menu->itemCount = 0;
}
}
// ============================================================ // ============================================================
// wmMenuFindItem -- find menu item by command ID across all menus // wmMenuFindItem -- find menu item by command ID across all menus
// ============================================================ // ============================================================
@ -1291,7 +1323,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
memset(win->contentBuf, 0xFF, bufSize); memset(win->contentBuf, 0xFF, bufSize);
} }
win->needsPaint = true; win->fullRepaint = true;
stack->windows[stack->count] = win; stack->windows[stack->count] = win;
stack->count++; stack->count++;
@ -1850,6 +1882,7 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
} }
if (win->onPaint) { if (win->onPaint) {
win->fullRepaint = true;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; win->contentDirty = true;
@ -2367,6 +2400,7 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_
} }
if (win->onPaint) { if (win->onPaint) {
win->fullRepaint = true;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; win->contentDirty = true;
@ -2420,6 +2454,7 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *
} }
if (win->onPaint) { if (win->onPaint) {
win->fullRepaint = true;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; win->contentDirty = true;

View file

@ -85,6 +85,12 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked
// Insert a horizontal separator line. Separators are not interactive. // Insert a horizontal separator line. Separators are not interactive.
void wmAddMenuSeparator(MenuT *menu); void wmAddMenuSeparator(MenuT *menu);
// Remove a menu item by command ID. Returns true if found and removed.
bool wmRemoveMenuItem(MenuT *menu, int32_t id);
// Remove all items from a menu (preserves the menu itself on the menu bar).
void wmClearMenuItems(MenuT *menu);
// Query or set the checked state of a menu item by command ID. // Query or set the checked state of a menu item by command ID.
// Searches all menus in the menu bar. For radio items, setting // Searches all menus in the menu bar. For radio items, setting
// checked=true also unchecks other radio items in the same group. // checked=true also unchecks other radio items in the same group.

View file

@ -90,17 +90,8 @@ static void sysInfoAppend(const char *fmt, ...);
static bool sHasMouseWheel = false; static bool sHasMouseWheel = false;
static int32_t sLastWheelDelta = 0; static int32_t sLastWheelDelta = 0;
// Software cursor tracking. Two modes detected at runtime: // Mouse coordinate range set by functions 07h/08h in platformMouseInit.
// Mickey mode (default): function 0Bh raw deltas accumulated into // Position read directly from function 03h (absolute coordinates).
// sCurX/sCurY. Works on 86Box and real hardware where function
// 03h coordinates may be wrong in VESA modes.
// Absolute mode: function 03h coordinates used directly. Auto-
// activated when function 0Bh returns zero deltas but function
// 03h position is changing (DOSBox-X seamless mouse).
static bool sAbsMouseMode = false;
static bool sDetecting = true;
static int32_t sPrevAbsX = -1;
static int32_t sPrevAbsY = -1;
static int32_t sMouseRangeW = 0; static int32_t sMouseRangeW = 0;
static int32_t sMouseRangeH = 0; static int32_t sMouseRangeH = 0;
static int32_t sCurX = 0; static int32_t sCurX = 0;
@ -1564,13 +1555,11 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
sCurX = screenW / 2; sCurX = screenW / 2;
sCurY = screenH / 2; sCurY = screenH / 2;
// Function 00h: reset driver, detect mouse hardware // Function 00h: reset driver
memset(&r, 0, sizeof(r)); memset(&r, 0, sizeof(r));
r.x.ax = 0x0000; r.x.ax = 0x0000;
__dpmi_int(0x33, &r); __dpmi_int(0x33, &r);
// Set coordinate range for function 03h. Always do this so that
// absolute mode works if we switch to it during detection.
// Function 07h: set horizontal range // Function 07h: set horizontal range
memset(&r, 0, sizeof(r)); memset(&r, 0, sizeof(r));
r.x.ax = 0x0007; r.x.ax = 0x0007;
@ -1592,10 +1581,6 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
r.x.dx = screenH / 2; r.x.dx = screenH / 2;
__dpmi_int(0x33, &r); __dpmi_int(0x33, &r);
// Flush any stale mickey counters so the first poll starts clean.
memset(&r, 0, sizeof(r));
r.x.ax = 0x000B;
__dpmi_int(0x33, &r);
} }
@ -1622,12 +1607,8 @@ void platformMouseSetAccel(int32_t threshold) {
// platformMousePoll // platformMousePoll
// ============================================================ // ============================================================
// //
// Reads button state via function 03h and raw mickey deltas via // Reads button state and cursor position via INT 33h function 03h.
// function 0Bh. Position is tracked in software (sCurX/sCurY) // Coordinate range was set by functions 07h/08h in platformMouseInit.
// rather than using the driver's coordinates, because many real-
// hardware mouse drivers cannot handle VESA mode coordinate ranges.
// Function 0Bh returns the accumulated mickey motion since the last
// call and is reliable across all drivers.
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
__dpmi_regs r; __dpmi_regs r;
@ -1645,45 +1626,8 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
sLastWheelDelta = (int32_t)(int8_t)(r.x.bx >> 8); sLastWheelDelta = (int32_t)(int8_t)(r.x.bx >> 8);
} }
int32_t absX = (int16_t)r.x.cx; sCurX = (int16_t)r.x.cx;
int32_t absY = (int16_t)r.x.dx; sCurY = (int16_t)r.x.dx;
// Function 0Bh: read mickey motion counters
memset(&r, 0, sizeof(r));
r.x.ax = 0x000B;
__dpmi_int(0x33, &r);
int16_t mickeyDx = (int16_t)r.x.cx;
int16_t mickeyDy = (int16_t)r.x.dx;
// Runtime detection: if mickeys are zero but the driver's own
// position changed, function 0Bh isn't generating deltas.
// Switch to absolute mode (DOSBox-X seamless mouse, etc.).
if (sDetecting) {
if (mickeyDx == 0 && mickeyDy == 0 &&
sPrevAbsX >= 0 && (absX != sPrevAbsX || absY != sPrevAbsY)) {
sAbsMouseMode = true;
sDetecting = false;
dvxLog("Mouse: absolute mode (no mickeys detected)");
}
// Once we get real mickeys, stop checking
if (mickeyDx != 0 || mickeyDy != 0) {
sDetecting = false;
dvxLog("Mouse: mickey mode (raw deltas detected)");
}
sPrevAbsX = absX;
sPrevAbsY = absY;
}
if (sAbsMouseMode) {
sCurX = absX;
sCurY = absY;
} else {
sCurX += mickeyDx;
sCurY += mickeyDy;
}
if (sCurX < 0) { if (sCurX < 0) {
sCurX = 0; sCurX = 0;

View file

@ -1,8 +1,8 @@
.topic sys.overview .topic sys.welcome
.title DVX System Overview .title Welcome!
.toc 0 System Overview .toc 0 Welcome!
.default .default
.h1 DVX System Overview .h1 Welcome!
DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, core API, libraries, and widget toolkit. DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, core API, libraries, and widget toolkit.

View file

@ -281,7 +281,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
if (sDragWidget && !(buttons & MOUSE_LEFT)) { if (sDragWidget && !(buttons & MOUSE_LEFT)) {
wclsOnDragEnd(sDragWidget, root, x, y); wclsOnDragEnd(sDragWidget, root, x, y);
wgtInvalidatePaint(root); wgtInvalidatePaint(sDragWidget);
sDragWidget = NULL; sDragWidget = NULL;
return; return;
} }
@ -311,7 +311,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
if (sDragWidget->wclass && (sDragWidget->wclass->flags & WCLASS_RELAYOUT_ON_SCROLL)) { if (sDragWidget->wclass && (sDragWidget->wclass->flags & WCLASS_RELAYOUT_ON_SCROLL)) {
wgtInvalidate(sDragWidget); wgtInvalidate(sDragWidget);
} else { } else {
wgtInvalidatePaint(root); wgtInvalidatePaint(sDragWidget);
} }
return; return;
@ -353,6 +353,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
} }
if (!(buttons & MOUSE_LEFT)) { if (!(buttons & MOUSE_LEFT)) {
sPrevMouseButtons = 0;
return; return;
} }
@ -399,10 +400,10 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
} }
} }
// Fire mouse event callbacks // Fire mouse event callbacks (content-relative coordinates)
if (hit->enabled) { if (hit->enabled) {
int32_t relX = vx - hit->x; int32_t relX = vx - hit->x - hit->contentOffX;
int32_t relY = vy - hit->y; int32_t relY = vy - hit->y - hit->contentOffY;
// MouseDown: button just pressed // MouseDown: button just pressed
if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) { if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) {
@ -518,8 +519,13 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
cd.clipW = win->contentW; cd.clipW = win->contentW;
cd.clipH = win->contentH; cd.clipH = win->contentH;
// Clear background bool full = win->fullRepaint;
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg); win->fullRepaint = false;
if (full) {
// Full repaint: clear background, relayout, paint everything
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg);
}
// Apply scroll offset and re-layout at virtual size // Apply scroll offset and re-layout at virtual size
int32_t scrollX = win->hScroll ? win->hScroll->value : 0; int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
@ -531,7 +537,10 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
root->y = -scrollY; root->y = -scrollY;
root->w = layoutW; root->w = layoutW;
root->h = layoutH; root->h = layoutH;
widgetLayoutChildren(root, &ctx->font);
if (full) {
widgetLayoutChildren(root, &ctx->font);
}
// Auto-focus first focusable widget if nothing has focus yet // Auto-focus first focusable widget if nothing has focus yet
if (!sFocusedWidget) { if (!sFocusedWidget) {
@ -542,8 +551,8 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
} }
} }
// Paint widget tree (clip rect limits drawing to visible area) // Paint widget tree (full = all widgets, partial = only dirty ones)
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, full);
// Paint overlay popups (dropdown/combobox) // Paint overlay popups (dropdown/combobox)
widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);

View file

@ -15,6 +15,8 @@
#include "dvxPlat.h" #include "dvxPlat.h"
#include "../widgets/box/box.h" #include "../widgets/box/box.h"
static bool sFullRepaint = false;
// ============================================================ // ============================================================
// debugContainerBorder // debugContainerBorder
@ -75,25 +77,32 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
return; return;
} }
// Paint this widget via vtable // On partial repaints (fullRepaint=false), only paint dirty widgets.
wclsPaint(w, d, ops, font, colors); // The window's fullRepaint flag is stored in sFullRepaint for the
// duration of the paint walk.
bool dirty = w->paintDirty || sFullRepaint;
w->paintDirty = false;
if (dirty) {
wclsPaint(w, d, ops, font, colors);
}
// Widgets that paint their own children return early // Widgets that paint their own children return early
if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) { if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
if (sDebugLayout) { if (sDebugLayout && dirty) {
debugContainerBorder(w, d, ops); debugContainerBorder(w, d, ops);
} }
return; return;
} }
// Paint 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);
} }
// Debug: draw container borders on top of children // Debug: draw container borders on top of children
if (sDebugLayout && w->firstChild) { if (sDebugLayout && dirty && w->firstChild) {
debugContainerBorder(w, d, ops); debugContainerBorder(w, d, ops);
} }
} }
@ -355,7 +364,8 @@ void wgtInvalidate(WidgetT *w) {
widgetManageScrollbars(w->window, ctx); widgetManageScrollbars(w->window, ctx);
} }
// Dirty the window -- dvxInvalidateWindow calls onPaint automatically // Full repaint — layout changed, all widgets need redrawing
w->window->fullRepaint = true;
dvxInvalidateWindow(ctx, w->window); dvxInvalidateWindow(ctx, w->window);
} }
@ -373,6 +383,9 @@ void wgtInvalidatePaint(WidgetT *w) {
return; return;
} }
// Mark only this widget as needing repaint
w->paintDirty = true;
WidgetT *root = w; WidgetT *root = w;
while (root->parent) { while (root->parent) {
@ -385,7 +398,7 @@ void wgtInvalidatePaint(WidgetT *w) {
return; return;
} }
// Dirty the window -- dvxInvalidateWindow calls onPaint automatically // Partial repaint — only dirty widgets will be repainted
dvxInvalidateWindow(ctx, w->window); dvxInvalidateWindow(ctx, w->window);
} }
@ -394,12 +407,14 @@ void wgtInvalidatePaint(WidgetT *w) {
// wgtPaint // wgtPaint
// ============================================================ // ============================================================
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, bool fullRepaint) {
if (!root) { if (!root) {
return; return;
} }
sFullRepaint = fullRepaint;
widgetPaintOne(root, d, ops, font, colors); widgetPaintOne(root, d, ops, font, colors);
sFullRepaint = false;
} }

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>DVX System Overview</title> <title>Welcome!</title>
<style> <style>
body { font-family: sans-serif; margin: 0; padding: 0; display: flex; } body { font-family: sans-serif; margin: 0; padding: 0; display: flex; }
nav { width: 250px; min-width: 250px; background: #f0f0f0; padding: 16px; nav { width: 250px; min-width: 250px; background: #f0f0f0; padding: 16px;
@ -29,7 +29,7 @@ img { max-width: 100%; }
<nav> <nav>
<h3>Contents</h3> <h3>Contents</h3>
<ul> <ul>
<li><a href="#sys.overview">System Overview</a></li> <li><a href="#sys.welcome">Welcome!</a></li>
<li><strong>Architecture</strong> <li><strong>Architecture</strong>
<ul> <ul>
<li><a href="#arch.overview">System Overview</a> <li><a href="#arch.overview">System Overview</a>
@ -761,9 +761,9 @@ img { max-width: 100%; }
</ul> </ul>
</nav> </nav>
<main> <main>
<div class="topic" id="sys.overview"> <div class="topic" id="sys.welcome">
<h1>DVX System Overview</h1> <h1>Welcome!</h1>
<h2>DVX System Overview</h2> <h2>Welcome!</h2>
<p>DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, core API, libraries, and widget toolkit.</p> <p>DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, core API, libraries, and widget toolkit.</p>
</div> </div>
<div class="topic" id="arch.overview"> <div class="topic" id="arch.overview">

View file

@ -0,0 +1,37 @@
[sdl]
windowresolution = 1024x768
[dosbox]
machine = svga_s3
memsize = 16
[cpu]
core = dynamic
#cputype = 486
#cpu_cycles = 25000
#cpu_cycles_protected = 25000
[render]
aspect = true
[dos]
umb = true
xms = true
ems = true
ver = 6.22
[mouse]
mouse_capture = seamless
dos_mouse_immediate = true
[serial]
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
[autoexec]
mount d ~/dos
rem d:\ctmouse\bin\ctmouse.exe /O
mount c .
c:
cd bin
dvx
rem exit

View file

@ -26,12 +26,15 @@ vesa oldvbe10 = false
umb = true umb = true
xms = true xms = true
ems = true ems = true
ver = 6.22
lfn = false
[serial] [serial]
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1 serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
[autoexec] [autoexec]
mount d ~/dos
mount c . mount c .
c: c:
cd bin cd bin
dir dvx

View file

@ -23,12 +23,8 @@
#include <strings.h> #include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
// Route stb_ds allocations through the tracking wrappers so that // The loader is not a DXE — use plain realloc/free for stb_ds so that
// arrput/arrfree in DXE code is tracked per-app. // all translation units (loaderMain.o, dvxPrefs.o) share the same heap.
extern void *dvxRealloc(void *ptr, size_t size);
extern void dvxFree(void *ptr);
#define STBDS_REALLOC(c, p, s) dvxRealloc((p), (s))
#define STBDS_FREE(c, p) dvxFree(p)
#define STB_DS_IMPLEMENTATION #define STB_DS_IMPLEMENTATION
#include "stb_ds_wrap.h" #include "stb_ds_wrap.h"
@ -240,7 +236,9 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
struct dirent *ent; struct dirent *ent;
while ((ent = readdir(dir)) != NULL) { while ((ent = readdir(dir)) != NULL) {
const char *name = ent->d_name; // Copy d_name — readdir may use a shared buffer across recursion
char name[DVX_MAX_PATH];
snprintf(name, sizeof(name), "%s", ent->d_name);
// Skip . and .. // Skip . and ..
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) { if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
@ -516,8 +514,12 @@ static int32_t countHcfFilesRecurse(const char *dirPath) {
continue; continue;
} }
// Copy d_name before any recursion — readdir may use a shared buffer
char name[DVX_MAX_PATH];
snprintf(name, sizeof(name), "%s", ent->d_name);
char fullPath[DVX_MAX_PATH]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name); snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
struct stat st; struct stat st;
@ -528,9 +530,9 @@ static int32_t countHcfFilesRecurse(const char *dirPath) {
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(ent->d_name); int32_t nameLen = (int32_t)strlen(name);
if (nameLen > 4 && strcasecmp(ent->d_name + nameLen - 4, ".hcf") == 0) { if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) {
count++; count++;
} }
} }
@ -585,8 +587,11 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
continue; continue;
} }
char name[DVX_MAX_PATH];
snprintf(name, sizeof(name), "%s", ent->d_name);
char fullPath[DVX_MAX_PATH]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, ent->d_name); snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, name);
struct stat st; struct stat st;
@ -598,8 +603,8 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
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, ent->d_name)) { } else if (platformGlobMatch(globPart, name)) {
if (excludePattern && platformGlobMatch(excludePattern, ent->d_name)) { if (excludePattern && platformGlobMatch(excludePattern, name)) {
continue; continue;
} }
@ -730,8 +735,12 @@ static void processHcfDir(const char *dirPath) {
continue; continue;
} }
// Copy d_name before any recursion — readdir may use a shared buffer
char name[DVX_MAX_PATH];
snprintf(name, sizeof(name), "%s", ent->d_name);
char fullPath[DVX_MAX_PATH]; char fullPath[DVX_MAX_PATH];
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name); snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
struct stat st; struct stat st;
@ -742,9 +751,9 @@ static void processHcfDir(const char *dirPath) {
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
processHcfDir(fullPath); processHcfDir(fullPath);
} else { } else {
int32_t nameLen = (int32_t)strlen(ent->d_name); int32_t nameLen = (int32_t)strlen(name);
if (nameLen > 4 && strcasecmp(ent->d_name + nameLen - 4, ".hcf") == 0) { if (nameLen > 4 && strcasecmp(name + nameLen - 4, ".hcf") == 0) {
processHcf(fullPath, dirPath); processHcf(fullPath, dirPath);
sSplashLoaded++; sSplashLoaded++;
splashUpdateProgress(); splashUpdateProgress();

5
run.sh
View file

@ -1,2 +1,5 @@
#!/bin/bash #!/bin/bash
flatpak run com.dosbox_x.DOSBox-X -conf dosbox-x.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

View file

@ -0,0 +1,17 @@
[Project]
Name = Icon Editor
Author = Scott Duensing
Company = Kangaroo Punch Studios
Version = 1.00
Copyright = Copyright 2026 Scott Duensing
Description = Icon editor for DVX.
[Modules]
Count = 0
[Forms]
File0 = iconed.frm
Count = 1
[Settings]
StartupForm = IconEd

View file

@ -0,0 +1,476 @@
VERSION DVX 1.00
Begin Form IconEd
Caption = "DVX Icon Editor"
Layout = VBox
AutoSize = True
Resizable = False
Centered = True
Begin Menu mnuFile
Caption = "&File"
Begin Menu mnuNew
Caption = "&New"
End
Begin Menu mnuOpen
Caption = "&Open..."
End
Begin Menu mnuSave
Caption = "&Save..."
End
Begin Menu mnuSep1
Caption = "-"
End
Begin Menu mnuExit
Caption = "E&xit"
End
End
Begin Menu mnuTools
Caption = "&Tools"
Begin Menu mnuPencil
Caption = "&Pencil"
Checked = True
RadioCheck = True
End
Begin Menu mnuEraser
Caption = "&Eraser"
RadioCheck = True
End
Begin Menu mnuPicker
Caption = "Color Pic&ker"
RadioCheck = True
End
End
Begin Menu mnuSize
Caption = "&Size"
Begin Menu mnuSize16
Caption = "16 x 16"
RadioCheck = True
End
Begin Menu mnuSize24
Caption = "24 x 24"
RadioCheck = True
End
Begin Menu mnuSize32
Caption = "32 x 32"
Checked = True
RadioCheck = True
End
End
Begin Frame fraMain
Caption = ""
Layout = HBox
Weight = 100
Begin PictureBox cvEditor
Width = 256
Height = 256
Weight = 0
End
Begin Frame fraSidebar
Caption = ""
Layout = VBox
Width = 100
Weight = 0
Begin Label lblPreview
Caption = "Preview:"
End
Begin PictureBox cvPreview
Width = 32
Height = 32
Weight = 0
End
Begin Label lblColor
Caption = "Color:"
End
Begin PictureBox cvSelColor
Width = 32
Height = 20
Weight = 0
End
Begin Label lblPalette
Caption = "Palette:"
End
Begin PictureBox cvPalette
Width = 80
Height = 80
Weight = 0
End
End
End
Begin Label lblStatus
Caption = "Ready. (0, 0)"
Height = 16
Weight = 0
End
End
OPTION EXPLICIT
CONST CANVAS_SIZE = 256
CONST BG_COLOR = &H00C0C0C0
CONST GRID_COLOR = &H00808080
CONST EDGE_COLOR = &H00404040
CONST TOOL_PENCIL = 0
CONST TOOL_ERASER = 1
CONST TOOL_PICKER = 2
CONST PAL_COLS = 8
CONST PAL_ROWS = 4
CONST PAL_CELL = 10
DIM pixels(31, 31) AS LONG
DIM palette(31) AS LONG
DIM iconW AS INTEGER
DIM iconH AS INTEGER
DIM pixelSize AS INTEGER
DIM curTool AS INTEGER
DIM curColor AS LONG
DIM drawing AS BOOLEAN
DIM dirty AS BOOLEAN
DIM filePath AS STRING
SUB InitPalette
palette(0) = &H00000000
palette(1) = &H00FFFFFF
palette(2) = &H00C0C0C0
palette(3) = &H00808080
palette(4) = &H00FF0000
palette(5) = &H00800000
palette(6) = &H00FF8000
palette(7) = &H00804000
palette(8) = &H00FFFF00
palette(9) = &H00808000
palette(10) = &H0000FF00
palette(11) = &H00008000
palette(12) = &H0000FFFF
palette(13) = &H00008080
palette(14) = &H0000FF80
palette(15) = &H00004040
palette(16) = &H000000FF
palette(17) = &H00000080
palette(18) = &H008000FF
palette(19) = &H00800080
palette(20) = &H00FF00FF
palette(21) = &H00FF0080
palette(22) = &H00FF80C0
palette(23) = &H004040FF
palette(24) = &H00404040
palette(25) = &H00A0A0A0
palette(26) = &H00E0E0E0
palette(27) = &H00FFE0C0
palette(28) = &H00C0FFE0
palette(29) = &H00C0E0FF
palette(30) = &H00FFC0C0
palette(31) = &H00C0C0FF
END SUB
SUB ClearPixels
DIM x AS INTEGER
DIM y AS INTEGER
FOR y = 0 TO 31
FOR x = 0 TO 31
pixels(x, y) = BG_COLOR
NEXT x
NEXT y
dirty = False
END SUB
SUB SetIconSize(w AS INTEGER)
iconW = w
iconH = w
pixelSize = CANVAS_SIZE \ w
END SUB
SUB DrawEditorGrid
DIM x AS INTEGER
DIM y AS INTEGER
DIM edgeW AS INTEGER
cvEditor.Clear BG_COLOR
FOR y = 0 TO iconH - 1
FOR x = 0 TO iconW - 1
cvEditor.FillRect x * pixelSize, y * pixelSize, pixelSize, pixelSize, pixels(x, y)
NEXT x
NEXT y
FOR x = 0 TO iconW
cvEditor.DrawLine x * pixelSize, 0, x * pixelSize, iconH * pixelSize, GRID_COLOR
NEXT x
FOR y = 0 TO iconH
cvEditor.DrawLine 0, y * pixelSize, iconW * pixelSize, y * pixelSize, GRID_COLOR
NEXT y
edgeW = iconW * pixelSize
cvEditor.DrawRect 0, 0, edgeW, edgeW, EDGE_COLOR
cvEditor.Refresh
END SUB
SUB DrawPreview
DIM x AS INTEGER
DIM y AS INTEGER
cvPreview.Clear BG_COLOR
FOR y = 0 TO iconH - 1
FOR x = 0 TO iconW - 1
cvPreview.SetPixel x, y, pixels(x, y)
NEXT x
NEXT y
cvPreview.Refresh
END SUB
SUB DrawPalette
DIM i AS INTEGER
DIM px AS INTEGER
DIM py AS INTEGER
cvPalette.Clear BG_COLOR
FOR i = 0 TO 31
px = (i MOD PAL_COLS) * PAL_CELL
py = (i \ PAL_COLS) * PAL_CELL
cvPalette.FillRect px, py, PAL_CELL, PAL_CELL, palette(i)
cvPalette.DrawRect px, py, PAL_CELL, PAL_CELL, &H00000000
NEXT i
cvPalette.Refresh
END SUB
SUB DrawSelectedColor
cvSelColor.Clear curColor
cvSelColor.Refresh
END SUB
SUB RefreshAll
DrawEditorGrid
DrawPreview
DrawSelectedColor
END SUB
SUB UpdateTitle
DIM t AS STRING
t = "DVX Icon Editor - " + STR$(iconW) + "x" + STR$(iconH)
IF filePath <> "" THEN
t = t + " - " + filePath
END IF
IF dirty THEN
t = t + " *"
END IF
IconEd.Caption = t
END SUB
SUB SetIconPixel(x AS INTEGER, y AS INTEGER)
DIM c AS LONG
IF x < 0 OR x >= iconW OR y < 0 OR y >= iconH THEN
EXIT SUB
END IF
IF curTool = TOOL_ERASER THEN
c = BG_COLOR
ELSE
c = curColor
END IF
IF pixels(x, y) <> c THEN
pixels(x, y) = c
dirty = True
UpdateTitle
cvEditor.FillRect x * pixelSize, y * pixelSize, pixelSize, pixelSize, c
cvEditor.DrawLine x * pixelSize, y * pixelSize, (x + 1) * pixelSize, y * pixelSize, GRID_COLOR
cvEditor.DrawLine x * pixelSize, y * pixelSize, x * pixelSize, (y + 1) * pixelSize, GRID_COLOR
cvEditor.DrawLine (x + 1) * pixelSize, y * pixelSize, (x + 1) * pixelSize, (y + 1) * pixelSize, GRID_COLOR
cvEditor.DrawLine x * pixelSize, (y + 1) * pixelSize, (x + 1) * pixelSize, (y + 1) * pixelSize, GRID_COLOR
cvPreview.SetPixel x, y, c
END IF
END SUB
SUB SelectTool(t AS INTEGER)
curTool = t
IF t = TOOL_PENCIL THEN
mnuPencil.Checked = True
lblStatus.Caption = "Tool: Pencil"
ELSEIF t = TOOL_ERASER THEN
mnuEraser.Checked = True
lblStatus.Caption = "Tool: Eraser"
ELSEIF t = TOOL_PICKER THEN
mnuPicker.Checked = True
lblStatus.Caption = "Tool: Picker"
END IF
END SUB
SUB ChangeSize(newSize AS INTEGER)
DIM ans AS INTEGER
IF newSize = iconW THEN
EXIT SUB
END IF
IF dirty THEN
ans = MsgBox("Unsaved changes will be lost. Change size?", vbYesNo)
IF ans = vbNo THEN
EXIT SUB
END IF
END IF
SetIconSize newSize
ClearPixels
filePath = ""
IF newSize = 16 THEN
mnuSize16.Checked = True
ELSEIF newSize = 24 THEN
mnuSize24.Checked = True
ELSE
mnuSize32.Checked = True
END IF
UpdateTitle
RefreshAll
lblStatus.Caption = STR$(iconW) + "x" + STR$(iconH) + " - Ready."
END SUB
SUB IconEd_Load
InitPalette
SetIconSize 32
ClearPixels
curTool = TOOL_PENCIL
curColor = &H00000000
drawing = False
filePath = ""
DrawPalette
RefreshAll
UpdateTitle
END SUB
SUB mnuNew_Click
DIM ans AS INTEGER
IF dirty THEN
ans = MsgBox("Unsaved changes. Start new icon?", vbYesNo)
IF ans = vbNo THEN
EXIT SUB
END IF
END IF
ClearPixels
filePath = ""
UpdateTitle
RefreshAll
END SUB
SUB mnuOpen_Click
DIM ans AS INTEGER
DIM path AS STRING
DIM x AS INTEGER
DIM y AS INTEGER
IF dirty THEN
ans = MsgBox("Unsaved changes. Open another file?", vbYesNo)
IF ans = vbNo THEN
EXIT SUB
END IF
END IF
path = InputBox$("Open BMP file:", "Open Icon", filePath)
IF path = "" THEN
EXIT SUB
END IF
cvPreview.Load path
FOR y = 0 TO iconH - 1
FOR x = 0 TO iconW - 1
pixels(x, y) = cvPreview.GetPixel(x, y)
NEXT x
NEXT y
filePath = path
dirty = False
UpdateTitle
RefreshAll
END SUB
SUB mnuSave_Click
DIM path AS STRING
IF filePath = "" THEN
path = InputBox$("Save BMP file:", "Save Icon", "ICON.BMP")
ELSE
path = InputBox$("Save BMP file:", "Save Icon", filePath)
END IF
IF path = "" THEN
EXIT SUB
END IF
DrawPreview
cvPreview.Save path
filePath = path
dirty = False
UpdateTitle
lblStatus.Caption = "Saved: " + path
END SUB
SUB mnuExit_Click
Unload IconEd
END SUB
SUB mnuPencil_Click
SelectTool TOOL_PENCIL
END SUB
SUB mnuEraser_Click
SelectTool TOOL_ERASER
END SUB
SUB mnuPicker_Click
SelectTool TOOL_PICKER
END SUB
SUB mnuSize16_Click
ChangeSize 16
END SUB
SUB mnuSize24_Click
ChangeSize 24
END SUB
SUB mnuSize32_Click
ChangeSize 32
END SUB
SUB cvEditor_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
DIM px AS INTEGER
DIM py AS INTEGER
px = x \ pixelSize
py = y \ pixelSize
IF curTool = TOOL_PICKER THEN
IF px >= 0 AND px < iconW AND py >= 0 AND py < iconH THEN
curColor = pixels(px, py)
DrawSelectedColor
SelectTool TOOL_PENCIL
END IF
EXIT SUB
END IF
drawing = True
SetIconPixel px, py
lblStatus.Caption = "(" + STR$(px) + "," + STR$(py) + ")"
END SUB
SUB cvEditor_MouseMove(button AS INTEGER, x AS INTEGER, y AS INTEGER)
DIM px AS INTEGER
DIM py AS INTEGER
px = x \ pixelSize
py = y \ pixelSize
IF px >= 0 AND px < iconW AND py >= 0 AND py < iconH THEN
lblStatus.Caption = "(" + STR$(px) + "," + STR$(py) + ")"
END IF
IF drawing THEN
SetIconPixel px, py
END IF
END SUB
SUB cvEditor_MouseUp(button AS INTEGER, x AS INTEGER, y AS INTEGER)
drawing = False
END SUB
SUB cvPalette_MouseDown(button AS INTEGER, x AS INTEGER, y AS INTEGER)
DIM col AS INTEGER
DIM row AS INTEGER
DIM idx AS INTEGER
col = x \ PAL_CELL
row = y \ PAL_CELL
IF col >= 0 AND col < PAL_COLS AND row >= 0 AND row < PAL_ROWS THEN
idx = row * PAL_COLS + col
curColor = palette(idx)
DrawSelectedColor
IF curTool = TOOL_PICKER THEN
SelectTool TOOL_PENCIL
END IF
END IF
END SUB

View file

@ -68,6 +68,8 @@ typedef struct {
char args[1024]; // launch arguments (empty if none) char args[1024]; // launch arguments (empty if none)
char helpFile[DVX_MAX_PATH]; // help file path (for F1 context help) char helpFile[DVX_MAX_PATH]; // help file path (for F1 context help)
char helpTopic[128]; // current help topic ID (updated by app) char helpTopic[128]; // current help topic ID (updated by app)
void (*onHelpQuery)(void *ctx); // called on F1 to refresh helpTopic
void *helpQueryCtx; // context for onHelpQuery
} DxeAppContextT; } DxeAppContextT;
// ============================================================ // ============================================================

View file

@ -106,12 +106,19 @@ static void f1HelpHandler(void *ctx) {
if (focusedAppId > 0) { if (focusedAppId > 0) {
ShellAppT *app = shellGetApp(focusedAppId); ShellAppT *app = shellGetApp(focusedAppId);
if (app && app->dxeCtx && app->dxeCtx->helpFile[0]) { if (app && app->dxeCtx) {
if (app->dxeCtx->helpTopic[0]) { // Let the app refresh helpTopic based on current context
snprintf(args, sizeof(args), "%s %s", if (app->dxeCtx->onHelpQuery) {
app->dxeCtx->helpFile, app->dxeCtx->helpTopic); app->dxeCtx->onHelpQuery(app->dxeCtx->helpQueryCtx);
} else { }
snprintf(args, sizeof(args), "%s", app->dxeCtx->helpFile);
if (app->dxeCtx->helpFile[0]) {
if (app->dxeCtx->helpTopic[0]) {
snprintf(args, sizeof(args), "%s %s",
app->dxeCtx->helpFile, app->dxeCtx->helpTopic);
} else {
snprintf(args, sizeof(args), "%s", app->dxeCtx->helpFile);
}
} }
} }
} }

View file

@ -130,6 +130,8 @@ void widgetFrameGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int3
// ============================================================ // ============================================================
void widgetFrameDestroy(WidgetT *w) { void widgetFrameDestroy(WidgetT *w) {
FrameDataT *fd = (FrameDataT *)w->data;
free((void *)fd->title);
free(w->data); free(w->data);
} }
@ -179,7 +181,7 @@ WidgetT *wgtFrame(WidgetT *parent, const char *title) {
if (w) { if (w) {
FrameDataT *fd = calloc(1, sizeof(FrameDataT)); FrameDataT *fd = calloc(1, sizeof(FrameDataT));
fd->title = title; fd->title = title ? strdup(title) : NULL;
fd->style = FrameInE; fd->style = FrameInE;
fd->color = 0; fd->color = 0;
w->data = fd; w->data = fd;

View file

@ -74,6 +74,8 @@ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
static void widgetButtonDestroy(WidgetT *w) { static void widgetButtonDestroy(WidgetT *w) {
ButtonDataT *d = (ButtonDataT *)w->data;
free((void *)d->text);
free(w->data); free(w->data);
w->data = NULL; w->data = NULL;
} }
@ -204,7 +206,8 @@ static void widgetButtonOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32
void widgetButtonSetText(WidgetT *w, const char *text) { void widgetButtonSetText(WidgetT *w, const char *text) {
ButtonDataT *d = (ButtonDataT *)w->data; ButtonDataT *d = (ButtonDataT *)w->data;
d->text = text; free((void *)d->text);
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }
@ -242,7 +245,7 @@ WidgetT *wgtButton(WidgetT *parent, const char *text) {
if (w) { if (w) {
ButtonDataT *d = calloc(1, sizeof(ButtonDataT)); ButtonDataT *d = calloc(1, sizeof(ButtonDataT));
w->data = d; w->data = d;
d->text = text; d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }

View file

@ -82,6 +82,69 @@ static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
} }
// Forward declarations for BASIC wrapper functions
void wgtCanvasClear(WidgetT *w, uint32_t color);
void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color);
uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y);
void wgtCanvasSetPenColor(WidgetT *w, uint32_t color);
void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height);
void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius);
// Convert a 0x00RRGGBB color to the display's packed pixel format.
static uint32_t rgbToPacked(WidgetT *w, uint32_t rgb) {
AppContextT *ctx = wgtGetContext(w);
if (!ctx) { return rgb; }
uint8_t r = (rgb >> 16) & 0xFF;
uint8_t g = (rgb >> 8) & 0xFF;
uint8_t b = rgb & 0xFF;
return packColor(&ctx->display, r, g, b);
}
// BASIC-callable wrappers that convert 0x00RRGGBB colors to packed format.
static void basClear(WidgetT *w, int32_t color) {
wgtCanvasClear(w, rgbToPacked(w, (uint32_t)color));
}
static void basSetPixel(WidgetT *w, int32_t x, int32_t y, int32_t color) {
wgtCanvasSetPixel(w, x, y, rgbToPacked(w, (uint32_t)color));
}
static uint32_t basGetPixel(const WidgetT *w, int32_t x, int32_t y) {
uint32_t packed = wgtCanvasGetPixel(w, x, y);
AppContextT *ctx = wgtGetContext((WidgetT *)w);
if (!ctx) { return packed; }
uint8_t r;
uint8_t g;
uint8_t b;
unpackColor(&ctx->display, packed, &r, &g, &b);
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
static void basDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1, int32_t color) {
wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
wgtCanvasDrawLine(w, x0, y0, x1, y1);
}
static void basDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color) {
wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
wgtCanvasDrawRect(w, x, y, width, height);
}
static void basFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height, int32_t color) {
wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
wgtCanvasFillRect(w, x, y, width, height);
}
static void basFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius, int32_t color) {
wgtCanvasSetPenColor(w, rgbToPacked(w, (uint32_t)color));
wgtCanvasFillCircle(w, cx, cy, radius);
}
// ============================================================ // ============================================================
// canvasDrawDot // canvasDrawDot
// ============================================================ // ============================================================
@ -714,6 +777,9 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
WidgetT *wgt = widgetAlloc(parent, sTypeId); WidgetT *wgt = widgetAlloc(parent, sTypeId);
if (wgt) { if (wgt) {
wgt->contentOffX = CANVAS_BORDER;
wgt->contentOffY = CANVAS_BORDER;
CanvasDataT *cd = (CanvasDataT *)calloc(1, sizeof(CanvasDataT)); CanvasDataT *cd = (CanvasDataT *)calloc(1, sizeof(CanvasDataT));
cd->pixelData = data; cd->pixelData = data;
@ -722,7 +788,7 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
cd->canvasPitch = pitch; cd->canvasPitch = pitch;
cd->canvasBpp = bpp; cd->canvasBpp = bpp;
cd->penColor = packColor(d, 0, 0, 0); cd->penColor = packColor(d, 0, 0, 0);
cd->penSize = 2; cd->penSize = 1;
cd->lastX = -1; cd->lastX = -1;
cd->lastY = -1; cd->lastY = -1;
wgt->data = cd; wgt->data = cd;
@ -734,6 +800,52 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
} }
void wgtCanvasResize(WidgetT *w, int32_t newW, int32_t newH) {
if (!w || w->type != sTypeId || newW <= 0 || newH <= 0) {
return;
}
CanvasDataT *cd = (CanvasDataT *)w->data;
if (!cd || (cd->canvasW == newW && cd->canvasH == newH)) {
return;
}
AppContextT *ctx = wgtGetContext(w);
if (!ctx) {
return;
}
const DisplayT *d = &ctx->display;
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = newW * bpp;
uint8_t *data = (uint8_t *)malloc(pitch * newH);
if (!data) {
return;
}
// Fill with white
uint32_t white = packColor(d, 255, 255, 255);
BlitOpsT canvasOps;
drawInit(&canvasOps, d);
for (int32_t y = 0; y < newH; y++) {
canvasOps.spanFill(data + y * pitch, white, newW);
}
free(cd->pixelData);
cd->pixelData = data;
cd->canvasW = newW;
cd->canvasH = newH;
cd->canvasPitch = pitch;
cd->canvasBpp = bpp;
wgtInvalidatePaint(w);
}
int32_t wgtCanvasLoad(WidgetT *w, const char *path) { int32_t wgtCanvasLoad(WidgetT *w, const char *path) {
if (!w || w->type != sTypeId || !path) { if (!w || w->type != sTypeId || !path) {
return -1; return -1;
@ -909,6 +1021,7 @@ static const struct {
void (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color); void (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color);
uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y); uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y);
void (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text); void (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text);
void (*resize)(WidgetT *w, int32_t newW, int32_t newH);
} sApi = { } sApi = {
.create = wgtCanvas, .create = wgtCanvas,
.clear = wgtCanvasClear, .clear = wgtCanvasClear,
@ -923,11 +1036,22 @@ static const struct {
.fillCircle = wgtCanvasFillCircle, .fillCircle = wgtCanvasFillCircle,
.setPixel = wgtCanvasSetPixel, .setPixel = wgtCanvasSetPixel,
.getPixel = wgtCanvasGetPixel, .getPixel = wgtCanvasGetPixel,
.drawText = wgtCanvasDrawText .drawText = wgtCanvasDrawText,
.resize = wgtCanvasResize
}; };
static const WgtMethodDescT sMethods[] = { static const WgtMethodDescT sMethods[] = {
{ "Clear", WGT_SIG_INT, (void *)wgtCanvasClear } { "Clear", WGT_SIG_INT, (void *)basClear },
{ "DrawLine", WGT_SIG_INT5, (void *)basDrawLine },
{ "DrawRect", WGT_SIG_INT5, (void *)basDrawRect },
{ "DrawText", WGT_SIG_INT_INT_STR,(void *)wgtCanvasDrawText },
{ "FillCircle", WGT_SIG_INT4, (void *)basFillCircle },
{ "FillRect", WGT_SIG_INT5, (void *)basFillRect },
{ "GetPixel", WGT_SIG_RET_INT_INT_INT, (void *)basGetPixel },
{ "Load", WGT_SIG_STR, (void *)wgtCanvasLoad },
{ "Save", WGT_SIG_STR, (void *)wgtCanvasSave },
{ "Resize", WGT_SIG_INT_INT, (void *)wgtCanvasResize },
{ "SetPixel", WGT_SIG_INT3, (void *)basSetPixel },
}; };
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
@ -935,7 +1059,7 @@ static const WgtIfaceT sIface = {
.props = NULL, .props = NULL,
.propCount = 0, .propCount = 0,
.methods = sMethods, .methods = sMethods,
.methodCount = 1, .methodCount = 11,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT_INT, .createSig = WGT_CREATE_PARENT_INT_INT,

View file

@ -64,6 +64,8 @@ void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
static void widgetCheckboxDestroy(WidgetT *w) { static void widgetCheckboxDestroy(WidgetT *w) {
CheckboxDataT *d = (CheckboxDataT *)w->data;
free((void *)d->text);
free(w->data); free(w->data);
w->data = NULL; w->data = NULL;
} }
@ -175,7 +177,8 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
void widgetCheckboxSetText(WidgetT *w, const char *text) { void widgetCheckboxSetText(WidgetT *w, const char *text) {
CheckboxDataT *d = (CheckboxDataT *)w->data; CheckboxDataT *d = (CheckboxDataT *)w->data;
d->text = text; free((void *)d->text);
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }
@ -211,7 +214,7 @@ WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
if (w) { if (w) {
CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT)); CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT));
w->data = d; w->data = d;
d->text = text; d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }

View file

@ -9,6 +9,11 @@ typedef struct {
void (*setItems)(WidgetT *w, const char **items, int32_t count); void (*setItems)(WidgetT *w, const char **items, int32_t count);
int32_t (*getSelected)(const WidgetT *w); int32_t (*getSelected)(const WidgetT *w);
void (*setSelected)(WidgetT *w, int32_t idx); void (*setSelected)(WidgetT *w, int32_t idx);
void (*addItem)(WidgetT *w, const char *text);
void (*removeItem)(WidgetT *w, int32_t idx);
void (*clear)(WidgetT *w);
const char *(*getItem)(const WidgetT *w, int32_t idx);
int32_t (*getItemCount)(const WidgetT *w);
} ComboBoxApiT; } ComboBoxApiT;
static inline const ComboBoxApiT *dvxComboBoxApi(void) { static inline const ComboBoxApiT *dvxComboBoxApi(void) {
@ -21,5 +26,10 @@ static inline const ComboBoxApiT *dvxComboBoxApi(void) {
#define wgtComboBoxSetItems(w, items, count) dvxComboBoxApi()->setItems(w, items, count) #define wgtComboBoxSetItems(w, items, count) dvxComboBoxApi()->setItems(w, items, count)
#define wgtComboBoxGetSelected(w) dvxComboBoxApi()->getSelected(w) #define wgtComboBoxGetSelected(w) dvxComboBoxApi()->getSelected(w)
#define wgtComboBoxSetSelected(w, idx) dvxComboBoxApi()->setSelected(w, idx) #define wgtComboBoxSetSelected(w, idx) dvxComboBoxApi()->setSelected(w, idx)
#define wgtComboBoxAddItem(w, text) dvxComboBoxApi()->addItem(w, text)
#define wgtComboBoxRemoveItem(w, idx) dvxComboBoxApi()->removeItem(w, idx)
#define wgtComboBoxClear(w) dvxComboBoxApi()->clear(w)
#define wgtComboBoxGetItem(w, idx) dvxComboBoxApi()->getItem(w, idx)
#define wgtComboBoxGetItemCount(w) dvxComboBoxApi()->getItemCount(w)
#endif // COMBOBOX_H #endif // COMBOBOX_H

View file

@ -24,6 +24,7 @@
#include "dvxWgtP.h" #include "dvxWgtP.h"
#include "../texthelp/textHelp.h" #include "../texthelp/textHelp.h"
#include "../listhelp/listHelp.h" #include "../listhelp/listHelp.h"
#include "stb_ds_wrap.h"
static int32_t sTypeId = -1; static int32_t sTypeId = -1;
@ -45,6 +46,9 @@ typedef struct {
int32_t hoverIdx; int32_t hoverIdx;
int32_t listScrollPos; int32_t listScrollPos;
int32_t maxItemLen; int32_t maxItemLen;
// Owned item storage for AddItem/RemoveItem/Clear
char **ownedItems; // stb_ds dynamic array of strdup'd strings
bool ownsItems; // true if items[] points to ownedItems
} ComboBoxDataT; } ComboBoxDataT;
@ -74,6 +78,10 @@ void widgetComboBoxDestroy(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data; ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (d) { if (d) {
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrfree(d->ownedItems);
free(d->buf); free(d->buf);
free(d->undoBuf); free(d->undoBuf);
free(d); free(d);
@ -520,6 +528,70 @@ void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
} }
// ============================================================
// Owned item management
// ============================================================
static void comboBoxSyncOwned(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
d->items = (const char **)d->ownedItems;
d->itemCount = (int32_t)arrlen(d->ownedItems);
d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount);
d->ownsItems = true;
wgtInvalidate(w);
}
void wgtComboBoxAddItem(WidgetT *w, const char *text) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
arrput(d->ownedItems, strdup(text ? text : ""));
comboBoxSyncOwned(w);
}
void wgtComboBoxClear(WidgetT *w) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrsetlen(d->ownedItems, 0);
comboBoxSyncOwned(w);
d->selectedIdx = -1;
d->listScrollPos = 0;
}
const char *wgtComboBoxGetItem(const WidgetT *w, int32_t idx) {
if (!w || w->type != sTypeId) { return ""; }
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
if (idx < 0 || idx >= d->itemCount) { return ""; }
return d->items[idx] ? d->items[idx] : "";
}
int32_t wgtComboBoxGetItemCount(const WidgetT *w) {
if (!w || w->type != sTypeId) { return 0; }
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
return d->itemCount;
}
void wgtComboBoxRemoveItem(WidgetT *w, int32_t idx) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
free(d->ownedItems[idx]);
arrdel(d->ownedItems, idx);
comboBoxSyncOwned(w);
if (d->selectedIdx >= d->itemCount) {
d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
}
}
// ============================================================ // ============================================================
// DXE registration // DXE registration
// ============================================================ // ============================================================
@ -530,23 +602,41 @@ static const struct {
void (*setItems)(WidgetT *w, const char **items, int32_t count); void (*setItems)(WidgetT *w, const char **items, int32_t count);
int32_t (*getSelected)(const WidgetT *w); int32_t (*getSelected)(const WidgetT *w);
void (*setSelected)(WidgetT *w, int32_t index); void (*setSelected)(WidgetT *w, int32_t index);
void (*addItem)(WidgetT *w, const char *text);
void (*removeItem)(WidgetT *w, int32_t idx);
void (*clear)(WidgetT *w);
const char *(*getItem)(const WidgetT *w, int32_t idx);
int32_t (*getItemCount)(const WidgetT *w);
} sApi = { } sApi = {
.create = wgtComboBox, .create = wgtComboBox,
.setItems = wgtComboBoxSetItems, .setItems = wgtComboBoxSetItems,
.getSelected = wgtComboBoxGetSelected, .getSelected = wgtComboBoxGetSelected,
.setSelected = wgtComboBoxSetSelected .setSelected = wgtComboBoxSetSelected,
.addItem = wgtComboBoxAddItem,
.removeItem = wgtComboBoxRemoveItem,
.clear = wgtComboBoxClear,
.getItem = wgtComboBoxGetItem,
.getItemCount = wgtComboBoxGetItemCount
}; };
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
{ "ListIndex", WGT_IFACE_INT, (void *)wgtComboBoxGetSelected, (void *)wgtComboBoxSetSelected, NULL } { "ListIndex", WGT_IFACE_INT, (void *)wgtComboBoxGetSelected, (void *)wgtComboBoxSetSelected, NULL }
}; };
static const WgtMethodDescT sMethods[] = {
{ "AddItem", WGT_SIG_STR, (void *)wgtComboBoxAddItem },
{ "Clear", WGT_SIG_VOID, (void *)wgtComboBoxClear },
{ "List", WGT_SIG_RET_STR_INT, (void *)wgtComboBoxGetItem },
{ "ListCount", WGT_SIG_RET_INT, (void *)wgtComboBoxGetItemCount },
{ "RemoveItem", WGT_SIG_INT, (void *)wgtComboBoxRemoveItem },
};
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
.basName = "ComboBox", .basName = "ComboBox",
.props = sProps, .props = sProps,
.propCount = 1, .propCount = 1,
.methods = NULL, .methods = sMethods,
.methodCount = 0, .methodCount = 5,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT, .createSig = WGT_CREATE_PARENT_INT,

View file

@ -9,6 +9,11 @@ typedef struct {
void (*setItems)(WidgetT *w, const char **items, int32_t count); void (*setItems)(WidgetT *w, const char **items, int32_t count);
int32_t (*getSelected)(const WidgetT *w); int32_t (*getSelected)(const WidgetT *w);
void (*setSelected)(WidgetT *w, int32_t idx); void (*setSelected)(WidgetT *w, int32_t idx);
void (*addItem)(WidgetT *w, const char *text);
void (*removeItem)(WidgetT *w, int32_t idx);
void (*clear)(WidgetT *w);
const char *(*getItem)(const WidgetT *w, int32_t idx);
int32_t (*getItemCount)(const WidgetT *w);
} DropdownApiT; } DropdownApiT;
static inline const DropdownApiT *dvxDropdownApi(void) { static inline const DropdownApiT *dvxDropdownApi(void) {
@ -17,9 +22,14 @@ static inline const DropdownApiT *dvxDropdownApi(void) {
return sApi; return sApi;
} }
#define wgtDropdown(parent) dvxDropdownApi()->create(parent) #define wgtDropdown(parent) dvxDropdownApi()->create(parent)
#define wgtDropdownSetItems(w, items, count) dvxDropdownApi()->setItems(w, items, count) #define wgtDropdownSetItems(w, items, count) dvxDropdownApi()->setItems(w, items, count)
#define wgtDropdownGetSelected(w) dvxDropdownApi()->getSelected(w) #define wgtDropdownGetSelected(w) dvxDropdownApi()->getSelected(w)
#define wgtDropdownSetSelected(w, idx) dvxDropdownApi()->setSelected(w, idx) #define wgtDropdownSetSelected(w, idx) dvxDropdownApi()->setSelected(w, idx)
#define wgtDropdownAddItem(w, text) dvxDropdownApi()->addItem(w, text)
#define wgtDropdownRemoveItem(w, idx) dvxDropdownApi()->removeItem(w, idx)
#define wgtDropdownClear(w) dvxDropdownApi()->clear(w)
#define wgtDropdownGetItem(w, idx) dvxDropdownApi()->getItem(w, idx)
#define wgtDropdownGetItemCount(w) dvxDropdownApi()->getItemCount(w)
#endif // DROPDOWN_H #endif // DROPDOWN_H

View file

@ -23,6 +23,10 @@
#include "../texthelp/textHelp.h" #include "../texthelp/textHelp.h"
#include "../listhelp/listHelp.h" #include "../listhelp/listHelp.h"
#include <stdlib.h>
#include <string.h>
#include "stb_ds_wrap.h"
static int32_t sTypeId = -1; static int32_t sTypeId = -1;
typedef struct { typedef struct {
@ -33,6 +37,9 @@ typedef struct {
int32_t hoverIdx; int32_t hoverIdx;
int32_t scrollPos; int32_t scrollPos;
int32_t maxItemLen; int32_t maxItemLen;
// Owned item storage for AddItem/RemoveItem/Clear
char **ownedItems; // stb_ds dynamic array of strdup'd strings
bool ownsItems; // true if items[] points to ownedItems
} DropdownDataT; } DropdownDataT;
@ -41,7 +48,16 @@ typedef struct {
// ============================================================ // ============================================================
void widgetDropdownDestroy(WidgetT *w) { void widgetDropdownDestroy(WidgetT *w) {
free(w->data); DropdownDataT *d = (DropdownDataT *)w->data;
if (d) {
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrfree(d->ownedItems);
free(d);
w->data = NULL;
}
} }
@ -392,6 +408,89 @@ void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
} }
// ============================================================
// dropdownSyncOwned
// ============================================================
static void dropdownSyncOwned(WidgetT *w) {
DropdownDataT *d = (DropdownDataT *)w->data;
d->items = (const char **)d->ownedItems;
d->itemCount = (int32_t)arrlen(d->ownedItems);
d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount);
d->ownsItems = true;
wgtInvalidate(w);
}
// ============================================================
// wgtDropdownAddItem
// ============================================================
void wgtDropdownAddItem(WidgetT *w, const char *text) {
VALIDATE_WIDGET_VOID(w, sTypeId);
DropdownDataT *d = (DropdownDataT *)w->data;
arrput(d->ownedItems, strdup(text ? text : ""));
dropdownSyncOwned(w);
}
// ============================================================
// wgtDropdownClear
// ============================================================
void wgtDropdownClear(WidgetT *w) {
VALIDATE_WIDGET_VOID(w, sTypeId);
DropdownDataT *d = (DropdownDataT *)w->data;
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrsetlen(d->ownedItems, 0);
dropdownSyncOwned(w);
d->selectedIdx = -1;
d->scrollPos = 0;
}
// ============================================================
// wgtDropdownGetItem
// ============================================================
const char *wgtDropdownGetItem(const WidgetT *w, int32_t idx) {
if (!w || w->type != sTypeId) { return ""; }
const DropdownDataT *d = (const DropdownDataT *)w->data;
if (idx < 0 || idx >= d->itemCount) { return ""; }
return d->items[idx] ? d->items[idx] : "";
}
// ============================================================
// wgtDropdownGetItemCount
// ============================================================
int32_t wgtDropdownGetItemCount(const WidgetT *w) {
if (!w || w->type != sTypeId) { return 0; }
const DropdownDataT *d = (const DropdownDataT *)w->data;
return d->itemCount;
}
// ============================================================
// wgtDropdownRemoveItem
// ============================================================
void wgtDropdownRemoveItem(WidgetT *w, int32_t idx) {
VALIDATE_WIDGET_VOID(w, sTypeId);
DropdownDataT *d = (DropdownDataT *)w->data;
if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
free(d->ownedItems[idx]);
arrdel(d->ownedItems, idx);
dropdownSyncOwned(w);
if (d->selectedIdx >= d->itemCount) {
d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
}
}
// ============================================================ // ============================================================
// DXE registration // DXE registration
// ============================================================ // ============================================================
@ -402,23 +501,41 @@ static const struct {
void (*setItems)(WidgetT *w, const char **items, int32_t count); void (*setItems)(WidgetT *w, const char **items, int32_t count);
int32_t (*getSelected)(const WidgetT *w); int32_t (*getSelected)(const WidgetT *w);
void (*setSelected)(WidgetT *w, int32_t index); void (*setSelected)(WidgetT *w, int32_t index);
void (*addItem)(WidgetT *w, const char *text);
void (*removeItem)(WidgetT *w, int32_t idx);
void (*clear)(WidgetT *w);
const char *(*getItem)(const WidgetT *w, int32_t idx);
int32_t (*getItemCount)(const WidgetT *w);
} sApi = { } sApi = {
.create = wgtDropdown, .create = wgtDropdown,
.setItems = wgtDropdownSetItems, .setItems = wgtDropdownSetItems,
.getSelected = wgtDropdownGetSelected, .getSelected = wgtDropdownGetSelected,
.setSelected = wgtDropdownSetSelected .setSelected = wgtDropdownSetSelected,
.addItem = wgtDropdownAddItem,
.removeItem = wgtDropdownRemoveItem,
.clear = wgtDropdownClear,
.getItem = wgtDropdownGetItem,
.getItemCount = wgtDropdownGetItemCount
}; };
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
{ "ListIndex", WGT_IFACE_INT, (void *)wgtDropdownGetSelected, (void *)wgtDropdownSetSelected, NULL } { "ListIndex", WGT_IFACE_INT, (void *)wgtDropdownGetSelected, (void *)wgtDropdownSetSelected, NULL }
}; };
static const WgtMethodDescT sMethods[] = {
{ "AddItem", WGT_SIG_STR, (void *)wgtDropdownAddItem },
{ "Clear", WGT_SIG_VOID, (void *)wgtDropdownClear },
{ "List", WGT_SIG_RET_STR_INT, (void *)wgtDropdownGetItem },
{ "ListCount", WGT_SIG_RET_INT, (void *)wgtDropdownGetItemCount },
{ "RemoveItem", WGT_SIG_INT, (void *)wgtDropdownRemoveItem },
};
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
.basName = "DropDown", .basName = "DropDown",
.props = sProps, .props = sProps,
.propCount = 1, .propCount = 1,
.methods = NULL, .methods = sMethods,
.methodCount = 0, .methodCount = 5,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT, .createSig = WGT_CREATE_PARENT,

View file

@ -7,10 +7,8 @@
// the Win3.1 convention where labels act as keyboard shortcuts for adjacent // the Win3.1 convention where labels act as keyboard shortcuts for adjacent
// controls (e.g., a label "&Name:" before a text input). // controls (e.g., a label "&Name:" before a text input).
// //
// The text pointer is stored directly (not copied) -- the caller must ensure // The widget owns its text string (strdup'd on set, freed on destroy).
// the string remains valid for the widget's lifetime, or use widgetLabelSetText // Callers can pass transient strings safely.
// to update it. This avoids unnecessary allocations for the common case of
// literal string labels.
// //
// Background is transparent by default (bgColor == 0 means use the parent's // Background is transparent by default (bgColor == 0 means use the parent's
// content background color from the color scheme). The +1 in calcMinH adds a // content background color from the color scheme). The +1 in calcMinH adds a
@ -49,6 +47,8 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
static void widgetLabelDestroy(WidgetT *w) { static void widgetLabelDestroy(WidgetT *w) {
LabelDataT *d = (LabelDataT *)w->data;
free((void *)d->text);
free(w->data); free(w->data);
w->data = NULL; w->data = NULL;
} }
@ -98,7 +98,8 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
void widgetLabelSetText(WidgetT *w, const char *text) { void widgetLabelSetText(WidgetT *w, const char *text) {
LabelDataT *d = (LabelDataT *)w->data; LabelDataT *d = (LabelDataT *)w->data;
d->text = text; free((void *)d->text);
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }
@ -131,7 +132,7 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text) {
if (w) { if (w) {
LabelDataT *d = calloc(1, sizeof(LabelDataT)); LabelDataT *d = calloc(1, sizeof(LabelDataT));
w->data = d; w->data = d;
d->text = text; d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }

View file

@ -15,6 +15,11 @@ typedef struct {
void (*selectAll)(WidgetT *w); void (*selectAll)(WidgetT *w);
void (*clearSelection)(WidgetT *w); void (*clearSelection)(WidgetT *w);
void (*setReorderable)(WidgetT *w, bool reorderable); void (*setReorderable)(WidgetT *w, bool reorderable);
void (*addItem)(WidgetT *w, const char *text);
void (*removeItem)(WidgetT *w, int32_t idx);
void (*clear)(WidgetT *w);
const char *(*getItem)(const WidgetT *w, int32_t idx);
int32_t (*getItemCount)(const WidgetT *w);
} ListBoxApiT; } ListBoxApiT;
static inline const ListBoxApiT *dvxListBoxApi(void) { static inline const ListBoxApiT *dvxListBoxApi(void) {

View file

@ -49,10 +49,14 @@ typedef struct {
int32_t dropIdx; int32_t dropIdx;
int32_t sbDragOrient; int32_t sbDragOrient;
int32_t sbDragOff; int32_t sbDragOff;
// Owned item storage for AddItem/RemoveItem/Clear
char **ownedItems; // stb_ds dynamic array of strdup'd strings
bool ownsItems; // true if items[] points to ownedItems
} ListBoxDataT; } ListBoxDataT;
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "stb_ds_wrap.h"
#define LISTBOX_PAD 2 #define LISTBOX_PAD 2
#define LISTBOX_MIN_ROWS 4 #define LISTBOX_MIN_ROWS 4
@ -136,6 +140,10 @@ void widgetListBoxDestroy(WidgetT *w) {
ListBoxDataT *d = (ListBoxDataT *)w->data; ListBoxDataT *d = (ListBoxDataT *)w->data;
if (d) { if (d) {
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrfree(d->ownedItems);
free(d->selBits); free(d->selBits);
free(d); free(d);
w->data = NULL; w->data = NULL;
@ -780,6 +788,66 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
} }
static void listBoxSyncOwned(WidgetT *w) {
ListBoxDataT *d = (ListBoxDataT *)w->data;
d->items = (const char **)d->ownedItems;
d->itemCount = (int32_t)arrlen(d->ownedItems);
d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount);
d->ownsItems = true;
listBoxAllocSelBits(w);
wgtInvalidate(w);
}
void wgtListBoxAddItem(WidgetT *w, const char *text) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListBoxDataT *d = (ListBoxDataT *)w->data;
arrput(d->ownedItems, strdup(text ? text : ""));
listBoxSyncOwned(w);
}
void wgtListBoxRemoveItem(WidgetT *w, int32_t idx) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListBoxDataT *d = (ListBoxDataT *)w->data;
if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
free(d->ownedItems[idx]);
arrdel(d->ownedItems, idx);
listBoxSyncOwned(w);
if (d->selectedIdx >= d->itemCount) {
d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
}
}
void wgtListBoxClear(WidgetT *w) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListBoxDataT *d = (ListBoxDataT *)w->data;
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrsetlen(d->ownedItems, 0);
listBoxSyncOwned(w);
d->selectedIdx = -1;
d->scrollPos = 0;
}
const char *wgtListBoxGetItem(const WidgetT *w, int32_t idx) {
if (!w || w->type != sTypeId) { return ""; }
const ListBoxDataT *d = (const ListBoxDataT *)w->data;
if (idx < 0 || idx >= d->itemCount) { return ""; }
return d->items[idx] ? d->items[idx] : "";
}
int32_t wgtListBoxGetItemCount(const WidgetT *w) {
if (!w || w->type != sTypeId) { return 0; }
const ListBoxDataT *d = (const ListBoxDataT *)w->data;
return d->itemCount;
}
void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) { void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) {
VALIDATE_WIDGET_VOID(w, sTypeId); VALIDATE_WIDGET_VOID(w, sTypeId);
@ -844,6 +912,11 @@ static const struct {
void (*selectAll)(WidgetT *w); void (*selectAll)(WidgetT *w);
void (*clearSelection)(WidgetT *w); void (*clearSelection)(WidgetT *w);
void (*setReorderable)(WidgetT *w, bool reorderable); void (*setReorderable)(WidgetT *w, bool reorderable);
void (*addItem)(WidgetT *w, const char *text);
void (*removeItem)(WidgetT *w, int32_t idx);
void (*clear)(WidgetT *w);
const char *(*getItem)(const WidgetT *w, int32_t idx);
int32_t (*getItemCount)(const WidgetT *w);
} sApi = { } sApi = {
.create = wgtListBox, .create = wgtListBox,
.setItems = wgtListBoxSetItems, .setItems = wgtListBoxSetItems,
@ -854,7 +927,12 @@ static const struct {
.setItemSelected = wgtListBoxSetItemSelected, .setItemSelected = wgtListBoxSetItemSelected,
.selectAll = wgtListBoxSelectAll, .selectAll = wgtListBoxSelectAll,
.clearSelection = wgtListBoxClearSelection, .clearSelection = wgtListBoxClearSelection,
.setReorderable = wgtListBoxSetReorderable .setReorderable = wgtListBoxSetReorderable,
.addItem = wgtListBoxAddItem,
.removeItem = wgtListBoxRemoveItem,
.clear = wgtListBoxClear,
.getItem = wgtListBoxGetItem,
.getItemCount = wgtListBoxGetItemCount
}; };
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
@ -862,12 +940,17 @@ static const WgtPropDescT sProps[] = {
}; };
static const WgtMethodDescT sMethods[] = { static const WgtMethodDescT sMethods[] = {
{ "SelectAll", WGT_SIG_VOID, (void *)wgtListBoxSelectAll }, { "AddItem", WGT_SIG_STR, (void *)wgtListBoxAddItem },
{ "Clear", WGT_SIG_VOID, (void *)wgtListBoxClear },
{ "ClearSelection", WGT_SIG_VOID, (void *)wgtListBoxClearSelection }, { "ClearSelection", WGT_SIG_VOID, (void *)wgtListBoxClearSelection },
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListBoxIsItemSelected },
{ "List", WGT_SIG_RET_STR_INT, (void *)wgtListBoxGetItem },
{ "ListCount", WGT_SIG_RET_INT, (void *)wgtListBoxGetItemCount },
{ "RemoveItem", WGT_SIG_INT, (void *)wgtListBoxRemoveItem },
{ "SelectAll", WGT_SIG_VOID, (void *)wgtListBoxSelectAll },
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListBoxSetItemSelected },
{ "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListBoxSetMultiSelect }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListBoxSetMultiSelect },
{ "SetReorderable", WGT_SIG_BOOL, (void *)wgtListBoxSetReorderable }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListBoxSetReorderable },
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListBoxIsItemSelected },
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListBoxSetItemSelected }
}; };
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
@ -875,7 +958,7 @@ static const WgtIfaceT sIface = {
.props = sProps, .props = sProps,
.propCount = 1, .propCount = 1,
.methods = sMethods, .methods = sMethods,
.methodCount = 6, .methodCount = 11,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT, .createSig = WGT_CREATE_PARENT,

View file

@ -22,6 +22,12 @@ typedef struct {
void (*selectAll)(WidgetT *w); void (*selectAll)(WidgetT *w);
void (*clearSelection)(WidgetT *w); void (*clearSelection)(WidgetT *w);
void (*setReorderable)(WidgetT *w, bool reorderable); void (*setReorderable)(WidgetT *w, bool reorderable);
void (*addItem)(WidgetT *w, const char *text);
void (*removeRow)(WidgetT *w, int32_t row);
void (*clear)(WidgetT *w);
const char *(*getCell)(const WidgetT *w, int32_t row, int32_t col);
void (*setCell)(WidgetT *w, int32_t row, int32_t col, const char *text);
int32_t (*getRowCount)(const WidgetT *w);
} ListViewApiT; } ListViewApiT;
static inline const ListViewApiT *dvxListViewApi(void) { static inline const ListViewApiT *dvxListViewApi(void) {
@ -43,5 +49,11 @@ static inline const ListViewApiT *dvxListViewApi(void) {
#define wgtListViewSelectAll(w) dvxListViewApi()->selectAll(w) #define wgtListViewSelectAll(w) dvxListViewApi()->selectAll(w)
#define wgtListViewClearSelection(w) dvxListViewApi()->clearSelection(w) #define wgtListViewClearSelection(w) dvxListViewApi()->clearSelection(w)
#define wgtListViewSetReorderable(w, reorderable) dvxListViewApi()->setReorderable(w, reorderable) #define wgtListViewSetReorderable(w, reorderable) dvxListViewApi()->setReorderable(w, reorderable)
#define wgtListViewAddItem(w, text) dvxListViewApi()->addItem(w, text)
#define wgtListViewRemoveRow(w, row) dvxListViewApi()->removeRow(w, row)
#define wgtListViewClear(w) dvxListViewApi()->clear(w)
#define wgtListViewGetCell(w, row, col) dvxListViewApi()->getCell(w, row, col)
#define wgtListViewSetCell(w, row, col, text) dvxListViewApi()->setCell(w, row, col, text)
#define wgtListViewGetRowCount(w) dvxListViewApi()->getRowCount(w)
#endif // LISTVIEW_H #endif // LISTVIEW_H

View file

@ -59,6 +59,7 @@ static int32_t sTypeId = -1;
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "stb_ds_wrap.h"
#define LISTVIEW_MAX_COLS 16 #define LISTVIEW_MAX_COLS 16
#define LISTVIEW_PAD 3 #define LISTVIEW_PAD 3
@ -96,6 +97,10 @@ typedef struct {
bool resizeDragging; bool resizeDragging;
int32_t sbDragOrient; int32_t sbDragOrient;
int32_t sbDragOff; int32_t sbDragOff;
// Owned cell storage for AddItem/RemoveRow/Clear/SetCell
char **ownedCells; // stb_ds dynamic array of strdup'd strings
bool ownsCells; // true if cellData points to ownedCells
int32_t nextCell; // next cell position for AddItem (row-major index)
} ListViewDataT; } ListViewDataT;
@ -105,6 +110,7 @@ typedef struct {
static void allocListViewSelBits(WidgetT *w); static void allocListViewSelBits(WidgetT *w);
static void listViewBuildSortIndex(WidgetT *w); static void listViewBuildSortIndex(WidgetT *w);
static void listViewSyncOwned(WidgetT *w);
static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font);
static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
void wgtListViewSelectAll(WidgetT *w); void wgtListViewSelectAll(WidgetT *w);
@ -223,6 +229,27 @@ static void listViewBuildSortIndex(WidgetT *w) {
} }
// ============================================================
// listViewSyncOwned
// ============================================================
// Sync cellData/rowCount from ownedCells after mutation.
// Rebuilds sort index and selection bits, forces column width recalculation.
static void listViewSyncOwned(WidgetT *w) {
ListViewDataT *lv = (ListViewDataT *)w->data;
int32_t cellCount = (int32_t)arrlen(lv->ownedCells);
lv->cellData = (const char **)lv->ownedCells;
lv->rowCount = (lv->colCount > 0) ? cellCount / lv->colCount : 0;
lv->totalColW = 0;
lv->ownsCells = true;
listViewBuildSortIndex(w);
allocListViewSelBits(w);
wgtInvalidate(w);
}
// ============================================================ // ============================================================
// resolveColumnWidths // resolveColumnWidths
// ============================================================ // ============================================================
@ -338,6 +365,10 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
void widgetListViewDestroy(WidgetT *w) { void widgetListViewDestroy(WidgetT *w) {
ListViewDataT *lv = (ListViewDataT *)w->data; ListViewDataT *lv = (ListViewDataT *)w->data;
for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) {
free(lv->ownedCells[i]);
}
arrfree(lv->ownedCells);
free(lv->selBits); free(lv->selBits);
free(lv->sortIndex); free(lv->sortIndex);
free(lv); free(lv);
@ -1606,6 +1637,47 @@ WidgetT *wgtListView(WidgetT *parent) {
} }
void wgtListViewAddItem(WidgetT *w, const char *text) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data;
if (lv->colCount <= 0) {
return;
}
// If at a row boundary, add colCount empty cells to start a new row
if (lv->nextCell % lv->colCount == 0) {
for (int32_t c = 0; c < lv->colCount; c++) {
arrput(lv->ownedCells, strdup(""));
}
}
// Replace the next empty cell with the provided text
int32_t idx = lv->nextCell;
free(lv->ownedCells[idx]);
lv->ownedCells[idx] = strdup(text ? text : "");
lv->nextCell++;
listViewSyncOwned(w);
}
void wgtListViewClear(WidgetT *w) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data;
for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) {
free(lv->ownedCells[i]);
}
arrsetlen(lv->ownedCells, 0);
lv->nextCell = 0;
lv->selectedIdx = -1;
lv->scrollPos = 0;
lv->scrollPosH = 0;
listViewSyncOwned(w);
}
void wgtListViewClearSelection(WidgetT *w) { void wgtListViewClearSelection(WidgetT *w) {
if (!w || w->type != sTypeId) { if (!w || w->type != sTypeId) {
return; return;
@ -1622,6 +1694,32 @@ void wgtListViewClearSelection(WidgetT *w) {
} }
const char *wgtListViewGetCell(const WidgetT *w, int32_t row, int32_t col) {
if (!w || w->type != sTypeId) {
return "";
}
const ListViewDataT *lv = (const ListViewDataT *)w->data;
if (row < 0 || row >= lv->rowCount || col < 0 || col >= lv->colCount) {
return "";
}
const char *cell = lv->cellData[row * lv->colCount + col];
return cell ? cell : "";
}
int32_t wgtListViewGetRowCount(const WidgetT *w) {
if (!w || w->type != sTypeId) {
return 0;
}
const ListViewDataT *lv = (const ListViewDataT *)w->data;
return lv->rowCount;
}
int32_t wgtListViewGetSelected(const WidgetT *w) { int32_t wgtListViewGetSelected(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, -1); VALIDATE_WIDGET(w, sTypeId, -1);
const ListViewDataT *lv = (const ListViewDataT *)w->data; const ListViewDataT *lv = (const ListViewDataT *)w->data;
@ -1646,6 +1744,45 @@ bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) {
} }
void wgtListViewRemoveRow(WidgetT *w, int32_t row) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data;
if (row < 0 || row >= lv->rowCount || lv->colCount <= 0) {
return;
}
// Remove colCount cells starting at row * colCount
int32_t base = row * lv->colCount;
for (int32_t c = 0; c < lv->colCount; c++) {
free(lv->ownedCells[base + c]);
}
// Shift remaining cells down by colCount positions
int32_t totalCells = (int32_t)arrlen(lv->ownedCells);
for (int32_t i = base; i < totalCells - lv->colCount; i++) {
lv->ownedCells[i] = lv->ownedCells[i + lv->colCount];
}
arrsetlen(lv->ownedCells, totalCells - lv->colCount);
// Adjust nextCell if it was past the removed row
if (lv->nextCell > base + lv->colCount) {
lv->nextCell -= lv->colCount;
} else if (lv->nextCell > base) {
lv->nextCell = base;
}
listViewSyncOwned(w);
if (lv->selectedIdx >= lv->rowCount) {
lv->selectedIdx = lv->rowCount > 0 ? lv->rowCount - 1 : -1;
}
}
void wgtListViewSelectAll(WidgetT *w) { void wgtListViewSelectAll(WidgetT *w) {
if (!w || w->type != sTypeId) { if (!w || w->type != sTypeId) {
return; return;
@ -1662,6 +1799,29 @@ void wgtListViewSelectAll(WidgetT *w) {
} }
void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data;
if (row < 0 || row >= lv->rowCount || col < 0 || col >= lv->colCount) {
return;
}
if (!lv->ownsCells) {
return;
}
int32_t idx = row * lv->colCount + col;
free(lv->ownedCells[idx]);
lv->ownedCells[idx] = strdup(text ? text : "");
// cellData pointer may have been invalidated by prior arrput; refresh it
lv->cellData = (const char **)lv->ownedCells;
lv->totalColW = 0;
wgtInvalidate(w);
}
void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)) { void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)) {
VALIDATE_WIDGET_VOID(w, sTypeId); VALIDATE_WIDGET_VOID(w, sTypeId);
ListViewDataT *lv = (ListViewDataT *)w->data; ListViewDataT *lv = (ListViewDataT *)w->data;
@ -1721,6 +1881,12 @@ static const struct {
void (*selectAll)(WidgetT *w); void (*selectAll)(WidgetT *w);
void (*clearSelection)(WidgetT *w); void (*clearSelection)(WidgetT *w);
void (*setReorderable)(WidgetT *w, bool reorderable); void (*setReorderable)(WidgetT *w, bool reorderable);
void (*addItem)(WidgetT *w, const char *text);
void (*removeRow)(WidgetT *w, int32_t row);
void (*clear)(WidgetT *w);
const char *(*getCell)(const WidgetT *w, int32_t row, int32_t col);
void (*setCell)(WidgetT *w, int32_t row, int32_t col, const char *text);
int32_t (*getRowCount)(const WidgetT *w);
} sApi = { } sApi = {
.create = wgtListView, .create = wgtListView,
.setColumns = wgtListViewSetColumns, .setColumns = wgtListViewSetColumns,
@ -1734,7 +1900,13 @@ static const struct {
.setItemSelected = wgtListViewSetItemSelected, .setItemSelected = wgtListViewSetItemSelected,
.selectAll = wgtListViewSelectAll, .selectAll = wgtListViewSelectAll,
.clearSelection = wgtListViewClearSelection, .clearSelection = wgtListViewClearSelection,
.setReorderable = wgtListViewSetReorderable .setReorderable = wgtListViewSetReorderable,
.addItem = wgtListViewAddItem,
.removeRow = wgtListViewRemoveRow,
.clear = wgtListViewClear,
.getCell = wgtListViewGetCell,
.setCell = wgtListViewSetCell,
.getRowCount = wgtListViewGetRowCount
}; };
static const WgtPropDescT sProps[] = { static const WgtPropDescT sProps[] = {
@ -1742,12 +1914,18 @@ static const WgtPropDescT sProps[] = {
}; };
static const WgtMethodDescT sMethods[] = { static const WgtMethodDescT sMethods[] = {
{ "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll }, { "AddItem", WGT_SIG_STR, (void *)wgtListViewAddItem },
{ "Clear", WGT_SIG_VOID, (void *)wgtListViewClear },
{ "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection }, { "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection },
{ "GetCell", WGT_SIG_RET_STR_INT_INT, (void *)wgtListViewGetCell },
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected },
{ "RemoveItem", WGT_SIG_INT, (void *)wgtListViewRemoveRow },
{ "RowCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount },
{ "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll },
{ "SetCell", WGT_SIG_INT_INT_STR, (void *)wgtListViewSetCell },
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected },
{ "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect },
{ "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable },
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected },
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected }
}; };
static const WgtIfaceT sIface = { static const WgtIfaceT sIface = {
@ -1755,7 +1933,7 @@ static const WgtIfaceT sIface = {
.props = sProps, .props = sProps,
.propCount = 1, .propCount = 1,
.methods = sMethods, .methods = sMethods,
.methodCount = 6, .methodCount = 12,
.events = NULL, .events = NULL,
.eventCount = 0, .eventCount = 0,
.createSig = WGT_CREATE_PARENT, .createSig = WGT_CREATE_PARENT,

View file

@ -79,6 +79,8 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// ============================================================ // ============================================================
static void widgetRadioDestroy(WidgetT *w) { static void widgetRadioDestroy(WidgetT *w) {
RadioDataT *d = (RadioDataT *)w->data;
free((void *)d->text);
free(w->data); free(w->data);
w->data = NULL; w->data = NULL;
} }
@ -298,7 +300,8 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
void widgetRadioSetText(WidgetT *w, const char *text) { void widgetRadioSetText(WidgetT *w, const char *text) {
RadioDataT *d = (RadioDataT *)w->data; RadioDataT *d = (RadioDataT *)w->data;
d->text = text; free((void *)d->text);
d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
} }
@ -344,7 +347,7 @@ WidgetT *wgtRadio(WidgetT *parent, const char *text) {
if (w) { if (w) {
RadioDataT *d = calloc(1, sizeof(RadioDataT)); RadioDataT *d = calloc(1, sizeof(RadioDataT));
w->data = d; w->data = d;
d->text = text; d->text = text ? strdup(text) : NULL;
w->accelKey = accelParse(text); w->accelKey = accelParse(text);
// Auto-assign index based on position in parent // Auto-assign index based on position in parent

View file

@ -30,6 +30,7 @@ typedef struct {
void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx);
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);
} TextInputApiT; } TextInputApiT;
static inline const TextInputApiT *dvxTextInputApi(void) { static inline const TextInputApiT *dvxTextInputApi(void) {
@ -54,5 +55,6 @@ static inline const TextInputApiT *dvxTextInputApi(void) {
#define wgtTextAreaSetLineDecorator(w, fn, ctx) dvxTextInputApi()->setLineDecorator(w, fn, ctx) #define wgtTextAreaSetLineDecorator(w, fn, ctx) dvxTextInputApi()->setLineDecorator(w, fn, ctx)
#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)
#endif // TEXTINPT_H #endif // TEXTINPT_H

View file

@ -136,6 +136,15 @@ 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);
// Pre-allocated paint buffers (avoid 3KB stack alloc per visible line per frame)
uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN)
char *expandBuf; // tab-expanded text (MAX_COLORIZE_LEN)
uint8_t *syntaxBuf; // tab-expanded syntax colors (MAX_COLORIZE_LEN)
// Cached gutter width (recomputed only when line count changes)
int32_t cachedGutterW;
int32_t cachedGutterLines; // line count when cachedGutterW was computed
} TextAreaDataT; } TextAreaDataT;
#include <ctype.h> #include <ctype.h>
@ -185,6 +194,7 @@ static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row);
static int32_t textAreaLineStartCached(WidgetT *w, int32_t row); static int32_t textAreaLineStartCached(WidgetT *w, int32_t row);
static void textAreaRebuildCache(WidgetT *w); static void textAreaRebuildCache(WidgetT *w);
static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col); static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col);
static void textAreaOffToRowColFast(TextAreaDataT *ta, int32_t off, int32_t *row, int32_t *col);
static int32_t visualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW); static int32_t visualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW); static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
@ -734,8 +744,14 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
} }
int32_t totalLines = textAreaGetLineCount(w); int32_t totalLines = textAreaGetLineCount(w);
int32_t digits = 1;
int32_t temp = totalLines; // Return cached value if line count hasn't changed
if (ta->cachedGutterW > 0 && ta->cachedGutterLines == totalLines) {
return ta->cachedGutterW;
}
int32_t digits = 1;
int32_t temp = totalLines;
while (temp >= 10) { while (temp >= 10) {
temp /= 10; temp /= 10;
@ -746,7 +762,10 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
digits = 3; digits = 3;
} }
return (digits + 1) * font->charWidth; ta->cachedGutterW = (digits + 1) * font->charWidth;
ta->cachedGutterLines = totalLines;
return ta->cachedGutterW;
} }
@ -824,14 +843,167 @@ static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) {
static int32_t textAreaGetMaxLineLen(WidgetT *w) { static int32_t textAreaGetMaxLineLen(WidgetT *w) {
textAreaEnsureCache(w); textAreaEnsureCache(w);
return ((TextAreaDataT *)w->data)->cachedMaxLL; TextAreaDataT *ta = (TextAreaDataT *)w->data;
if (ta->cachedMaxLL < 0 && ta->cachedLines >= 0) {
// Recompute max from cached visual lengths
int32_t maxVL = 0;
for (int32_t i = 0; i < ta->cachedLines; i++) {
if (ta->lineVisLens[i] > maxVL) {
maxVL = ta->lineVisLens[i];
}
}
ta->cachedMaxLL = maxVL;
}
return ta->cachedMaxLL;
} }
static inline void textAreaDirtyCache(WidgetT *w) { static inline void textAreaDirtyCache(WidgetT *w) {
TextAreaDataT *ta = (TextAreaDataT *)w->data; TextAreaDataT *ta = (TextAreaDataT *)w->data;
ta->cachedLines = -1; ta->cachedLines = -1;
ta->cachedMaxLL = -1; ta->cachedMaxLL = -1;
ta->cachedGutterW = 0;
}
// Incrementally update the cache after inserting bytes at `off`.
// If the insertion contains newlines, falls back to a full rebuild.
// Otherwise, adjusts offsets and visual lengths in O(lines_after_cursor).
static void textAreaCacheInsert(WidgetT *w, int32_t off, int32_t insertLen) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
if (ta->cachedLines < 0 || insertLen <= 0) {
textAreaDirtyCache(w);
return;
}
// Check if inserted bytes contain newlines — if so, full rebuild
const char *buf = ta->buf;
for (int32_t i = off; i < off + insertLen && i < ta->len; i++) {
if (buf[i] == '\n') {
textAreaDirtyCache(w);
return;
}
}
// Find which line the insertion is on
int32_t line = 0;
for (line = ta->cachedLines - 1; line > 0; line--) {
if (ta->lineOffsets[line] <= off) {
break;
}
}
// Shift all subsequent line offsets
for (int32_t i = line + 1; i <= ta->cachedLines; i++) {
ta->lineOffsets[i] += insertLen;
}
// Recompute visual length of the affected line
int32_t lineOff = ta->lineOffsets[line];
int32_t lineEnd = (line + 1 <= ta->cachedLines) ? ta->lineOffsets[line + 1] : ta->len;
int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 4;
int32_t vc = 0;
for (int32_t i = lineOff; i < lineEnd && buf[i] != '\n'; i++) {
if (buf[i] == '\t') {
vc += tabW - (vc % tabW);
} else {
vc++;
}
}
ta->lineVisLens[line] = vc;
// Update max line length
if (vc > ta->cachedMaxLL) {
ta->cachedMaxLL = vc;
} else {
ta->cachedMaxLL = -1; // unknown, will recompute lazily
}
ta->cachedGutterW = 0;
}
// Incrementally update the cache after deleting bytes at `off`.
// If the deletion spans newlines, falls back to a full rebuild.
static void textAreaCacheDelete(WidgetT *w, int32_t off, int32_t deleteLen) {
TextAreaDataT *ta = (TextAreaDataT *)w->data;
if (ta->cachedLines < 0 || deleteLen <= 0) {
textAreaDirtyCache(w);
return;
}
// Check if deleted bytes contained newlines — check the buffer BEFORE deletion
// Since deletion already happened, we can't check. Fall back to dirty for safety
// unless we know it was a single non-newline character.
if (deleteLen > 1) {
textAreaDirtyCache(w);
return;
}
// Single character delete — check if a line was removed by seeing if
// line count decreased. Quick check: the character that WAS at `off`
// is now gone. If the current char at `off` merged two lines, rebuild.
// Simple heuristic: if the line count from offsets doesn't match after
// adjusting, rebuild.
// Find which line the deletion was on
int32_t line = 0;
for (line = ta->cachedLines - 1; line > 0; line--) {
if (ta->lineOffsets[line] <= off) {
break;
}
}
// Shift subsequent offsets
for (int32_t i = line + 1; i <= ta->cachedLines; i++) {
ta->lineOffsets[i] -= deleteLen;
}
// Verify the line structure is still valid
int32_t lineOff = ta->lineOffsets[line];
int32_t nextOff = (line + 1 <= ta->cachedLines) ? ta->lineOffsets[line + 1] : ta->len;
// If the next line's offset is now <= this line's, a newline was deleted
if (line + 1 <= ta->cachedLines && nextOff <= lineOff) {
textAreaDirtyCache(w);
return;
}
// Check that no newline appears before the expected boundary
const char *buf = ta->buf;
for (int32_t i = lineOff; i < nextOff && i < ta->len; i++) {
if (buf[i] == '\n') {
if (i < nextOff - 1) {
// Newline in the middle — structure changed
textAreaDirtyCache(w);
return;
}
}
}
// Recompute visual length of the affected line
int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 4;
int32_t vc = 0;
for (int32_t i = lineOff; i < nextOff && i < ta->len && buf[i] != '\n'; i++) {
if (buf[i] == '\t') {
vc += tabW - (vc % tabW);
} else {
vc++;
}
}
ta->lineVisLens[line] = vc;
ta->cachedMaxLL = -1; // recompute lazily
ta->cachedGutterW = 0;
} }
@ -903,6 +1075,33 @@ static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int3
} }
// O(log N) offset-to-row/col using binary search on cached line offsets.
// Falls back to linear scan if cache is not available.
static void textAreaOffToRowColFast(TextAreaDataT *ta, int32_t off, int32_t *row, int32_t *col) {
if (!ta->lineOffsets || ta->cachedLines < 0) {
textAreaOffToRowCol(ta->buf, off, row, col);
return;
}
// Binary search for the row containing 'off'
int32_t lo = 0;
int32_t hi = ta->cachedLines;
while (lo < hi) {
int32_t mid = (lo + hi + 1) / 2;
if (ta->lineOffsets[mid] <= off) {
lo = mid;
} else {
hi = mid - 1;
}
}
*row = lo;
*col = off - ta->lineOffsets[lo];
}
static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) { static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) {
TextAreaDataT *ta = (TextAreaDataT *)w->data; TextAreaDataT *ta = (TextAreaDataT *)w->data;
int32_t row = ta->cursorRow; int32_t row = ta->cursorRow;
@ -957,6 +1156,9 @@ void widgetTextAreaDestroy(WidgetT *w) {
free(ta->undoBuf); free(ta->undoBuf);
free(ta->lineOffsets); free(ta->lineOffsets);
free(ta->lineVisLens); free(ta->lineVisLens);
free(ta->rawSyntax);
free(ta->expandBuf);
free(ta->syntaxBuf);
free(ta); free(ta);
w->data = NULL; w->data = NULL;
} }
@ -1065,7 +1267,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (key == 1) { if (key == 1) {
*pSA = 0; *pSA = 0;
*pSC = *pLen; *pSC = *pLen;
textAreaOffToRowCol(buf, *pLen, pRow, pCol); textAreaOffToRowColFast(ta, *pLen, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
textAreaEnsureVisible(w, visRows, visCols); textAreaEnsureVisible(w, visRows, visCols);
wgtInvalidatePaint(w); wgtInvalidatePaint(w);
@ -1099,7 +1301,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
} }
@ -1112,7 +1314,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
memmove(buf + off + paste, buf + off, *pLen - off + 1); memmove(buf + off + paste, buf + off, *pLen - off + 1);
memcpy(buf + off, clip, paste); memcpy(buf + off, clip, paste);
*pLen += paste; *pLen += paste;
textAreaOffToRowCol(buf, off + paste, pRow, pCol); textAreaOffToRowColFast(ta, off + paste, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
} }
@ -1138,7 +1340,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
@ -1182,7 +1384,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
// Restore cursor // Restore cursor
int32_t restoreOff = ta->undoCursor < *pLen ? ta->undoCursor : *pLen; int32_t restoreOff = ta->undoCursor < *pLen ? ta->undoCursor : *pLen;
ta->undoCursor = tmpCursor; ta->undoCursor = tmpCursor;
textAreaOffToRowCol(buf, restoreOff, pRow, pCol); textAreaOffToRowColFast(ta, restoreOff, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
@ -1209,7 +1411,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
} }
@ -1259,7 +1461,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
@ -1272,12 +1474,18 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
if (off > 0) { if (off > 0) {
char deleted = buf[off - 1];
textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
memmove(buf + off - 1, buf + off, *pLen - off + 1); memmove(buf + off - 1, buf + off, *pLen - off + 1);
(*pLen)--; (*pLen)--;
textAreaOffToRowCol(buf, off - 1, pRow, pCol); textAreaOffToRowColFast(ta, off - 1, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
textAreaDirtyCache(w);
if (deleted == '\n') {
textAreaDirtyCache(w);
} else {
textAreaCacheDelete(w, off - 1, 1);
}
if (w->onChange) { if (w->onChange) {
w->onChange(w); w->onChange(w);
@ -1298,7 +1506,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
@ -1311,10 +1519,16 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
if (off < *pLen) { if (off < *pLen) {
char deleted = buf[off];
textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
memmove(buf + off, buf + off + 1, *pLen - off); memmove(buf + off, buf + off + 1, *pLen - off);
(*pLen)--; (*pLen)--;
textAreaDirtyCache(w);
if (deleted == '\n') {
textAreaDirtyCache(w);
} else {
textAreaCacheDelete(w, off, 1);
}
if (w->onChange) { if (w->onChange) {
w->onChange(w); w->onChange(w);
@ -1334,7 +1548,7 @@ navigation:
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
if (off > 0) { if (off > 0) {
textAreaOffToRowCol(buf, off - 1, pRow, pCol); textAreaOffToRowColFast(ta, off - 1, pRow, pCol);
} }
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
@ -1350,7 +1564,7 @@ navigation:
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
if (off < *pLen) { if (off < *pLen) {
textAreaOffToRowCol(buf, off + 1, pRow, pCol); textAreaOffToRowColFast(ta, off + 1, pRow, pCol);
} }
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
@ -1365,7 +1579,7 @@ navigation:
SEL_BEGIN(); SEL_BEGIN();
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
int32_t newOff = wordBoundaryLeft(buf, off); int32_t newOff = wordBoundaryLeft(buf, off);
textAreaOffToRowCol(buf, newOff, pRow, pCol); textAreaOffToRowColFast(ta, newOff, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
SEL_END(); SEL_END();
textAreaEnsureVisible(w, visRows, visCols); textAreaEnsureVisible(w, visRows, visCols);
@ -1378,7 +1592,7 @@ navigation:
SEL_BEGIN(); SEL_BEGIN();
int32_t off = CUR_OFF(); int32_t off = CUR_OFF();
int32_t newOff = wordBoundaryRight(buf, *pLen, off); int32_t newOff = wordBoundaryRight(buf, *pLen, off);
textAreaOffToRowCol(buf, newOff, pRow, pCol); textAreaOffToRowColFast(ta, newOff, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
SEL_END(); SEL_END();
textAreaEnsureVisible(w, visRows, visCols); textAreaEnsureVisible(w, visRows, visCols);
@ -1489,7 +1703,7 @@ navigation:
// Ctrl+End (scancode 0x75) // Ctrl+End (scancode 0x75)
if (key == (0x75 | 0x100)) { if (key == (0x75 | 0x100)) {
SEL_BEGIN(); SEL_BEGIN();
textAreaOffToRowCol(buf, *pLen, pRow, pCol); textAreaOffToRowColFast(ta, *pLen, pRow, pCol);
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
SEL_END(); SEL_END();
textAreaEnsureVisible(w, visRows, visCols); textAreaEnsureVisible(w, visRows, visCols);
@ -1507,7 +1721,7 @@ navigation:
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
} }
@ -1557,7 +1771,7 @@ navigation:
int32_t hi = SEL_HI(); int32_t hi = SEL_HI();
memmove(buf + lo, buf + hi, *pLen - hi + 1); memmove(buf + lo, buf + hi, *pLen - hi + 1);
*pLen -= (hi - lo); *pLen -= (hi - lo);
textAreaOffToRowCol(buf, lo, pRow, pCol); textAreaOffToRowColFast(ta, lo, pRow, pCol);
*pSA = -1; *pSA = -1;
*pSC = -1; *pSC = -1;
} }
@ -1570,10 +1784,11 @@ navigation:
(*pLen)++; (*pLen)++;
(*pCol)++; (*pCol)++;
ta->desiredCol = *pCol; ta->desiredCol = *pCol;
textAreaCacheInsert(w, off, 1);
} else {
textAreaDirtyCache(w);
} }
textAreaDirtyCache(w);
if (w->onChange) { if (w->onChange) {
w->onChange(w); w->onChange(w);
} }
@ -1803,7 +2018,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
int32_t weRow; int32_t weRow;
int32_t weCol; int32_t weCol;
textAreaOffToRowCol(ta->buf, we, &weRow, &weCol); textAreaOffToRowColFast(ta, we, &weRow, &weCol);
ta->cursorRow = weRow; ta->cursorRow = weRow;
ta->cursorCol = weCol; ta->cursorCol = weCol;
@ -1959,11 +2174,8 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
break; break;
} }
// Compute line length by scanning from lineOff (not from buffer start) // Use cached line length (O(1) lookup instead of O(lineLen) scan)
int32_t lineL = 0; int32_t lineL = textAreaLineLenCached(w, row);
while (lineOff + lineL < len && buf[lineOff + lineL] != '\n') {
lineL++;
}
int32_t drawY = textY + i * font->charHeight; int32_t drawY = textY + i * font->charHeight;
// Line decorator: background highlight and gutter indicators // Line decorator: background highlight and gutter indicators
@ -2015,8 +2227,10 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3; int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3;
// Compute syntax colors for the raw line first (before expansion) // Compute syntax colors for the raw line first (before expansion)
uint8_t rawSyntax[MAX_COLORIZE_LEN]; uint8_t *rawSyntax = ta->rawSyntax;
bool hasSyntax = false; char *expandBuf = ta->expandBuf;
uint8_t *syntaxBuf = ta->syntaxBuf;
bool hasSyntax = false;
if (ta->colorize && lineL > 0) { if (ta->colorize && lineL > 0) {
int32_t colorLen = lineL < MAX_COLORIZE_LEN ? lineL : MAX_COLORIZE_LEN; int32_t colorLen = lineL < MAX_COLORIZE_LEN ? lineL : MAX_COLORIZE_LEN;
@ -2024,10 +2238,6 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
ta->colorize(buf + lineOff, colorLen, rawSyntax, ta->colorizeCtx); ta->colorize(buf + lineOff, colorLen, rawSyntax, ta->colorizeCtx);
hasSyntax = true; hasSyntax = true;
} }
// Expand tabs: build visual text and expanded syntax buffers
char expandBuf[MAX_COLORIZE_LEN];
uint8_t syntaxBuf[MAX_COLORIZE_LEN];
int32_t expandLen = 0; int32_t expandLen = 0;
int32_t vc = 0; int32_t vc = 0;
@ -2768,6 +2978,12 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
ta->captureTabs = false; // default: Tab moves focus ta->captureTabs = false; // default: Tab moves focus
ta->useTabChar = true; // default: insert actual tab character ta->useTabChar = true; // default: insert actual tab character
ta->tabWidth = 3; // default: 3-space tab stops ta->tabWidth = 3; // default: 3-space tab stops
// Pre-allocate paint buffers (avoids 3KB stack alloc per visible line per frame)
ta->rawSyntax = (uint8_t *)malloc(MAX_COLORIZE_LEN);
ta->expandBuf = (char *)malloc(MAX_COLORIZE_LEN);
ta->syntaxBuf = (uint8_t *)malloc(MAX_COLORIZE_LEN);
w->weight = 100; w->weight = 100;
} }
@ -2915,6 +3131,37 @@ int32_t wgtTextAreaGetCursorLine(const WidgetT *w) {
} }
int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize) {
buf[0] = '\0';
if (!w || w->type != sTextAreaTypeId || bufSize < 2) {
return 0;
}
const TextAreaDataT *ta = (const TextAreaDataT *)w->data;
if (!ta || !ta->buf || ta->len == 0) {
return 0;
}
int32_t off = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
int32_t ws = wordStart(ta->buf, off);
int32_t we = wordEnd(ta->buf, ta->len, off);
int32_t len = we - ws;
if (len <= 0) {
return 0;
}
if (len >= bufSize) {
len = bufSize - 1;
}
memcpy(buf, ta->buf + ws, len);
buf[len] = '\0';
return len;
}
void wgtTextAreaSetLineDecorator(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx) { void wgtTextAreaSetLineDecorator(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx) {
if (!w || w->type != sTextAreaTypeId) { if (!w || w->type != sTextAreaTypeId) {
return; return;
@ -3026,7 +3273,7 @@ bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, boo
// Move cursor to start of match (forward) or end of match (backward) // Move cursor to start of match (forward) or end of match (backward)
int32_t cursorOff = forward ? pos : pos + needleLen; int32_t cursorOff = forward ? pos : pos + needleLen;
textAreaOffToRowCol(ta->buf, cursorOff, &ta->cursorRow, &ta->cursorCol); textAreaOffToRowColFast(ta, cursorOff, &ta->cursorRow, &ta->cursorCol);
ta->desiredCol = ta->cursorCol; ta->desiredCol = ta->cursorCol;
// Scroll to show the match // Scroll to show the match
@ -3109,7 +3356,7 @@ int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replac
// Clamp cursor if it's past the end // Clamp cursor if it's past the end
int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol); int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
if (cursorOff > ta->len) { if (cursorOff > ta->len) {
textAreaOffToRowCol(ta->buf, ta->len, &ta->cursorRow, &ta->cursorCol); textAreaOffToRowColFast(ta, ta->len, &ta->cursorRow, &ta->cursorCol);
} }
ta->desiredCol = ta->cursorCol; ta->desiredCol = ta->cursorCol;
@ -3147,6 +3394,7 @@ static const struct {
void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx);
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);
} sApi = { } sApi = {
.create = wgtTextInput, .create = wgtTextInput,
.password = wgtPasswordInput, .password = wgtPasswordInput,
@ -3163,7 +3411,8 @@ static const struct {
.replaceAll = wgtTextAreaReplaceAll, .replaceAll = wgtTextAreaReplaceAll,
.setLineDecorator = wgtTextAreaSetLineDecorator, .setLineDecorator = wgtTextAreaSetLineDecorator,
.getCursorLine = wgtTextAreaGetCursorLine, .getCursorLine = wgtTextAreaGetCursorLine,
.setGutterClick = wgtTextAreaSetGutterClickCallback .setGutterClick = wgtTextAreaSetGutterClickCallback,
.getWordAtCursor = wgtTextAreaGetWordAtCursor
}; };
// Per-type APIs for the designer // Per-type APIs for the designer

View file

@ -726,7 +726,8 @@ const char *widgetTreeItemGetText(const WidgetT *w) {
void widgetTreeItemSetText(WidgetT *w, const char *text) { void widgetTreeItemSetText(WidgetT *w, const char *text) {
TreeItemDataT *ti = (TreeItemDataT *)w->data; TreeItemDataT *ti = (TreeItemDataT *)w->data;
ti->text = text; free((void *)ti->text);
ti->text = text ? strdup(text) : NULL;
invalidateTreeDims(w); invalidateTreeDims(w);
} }
@ -1413,6 +1414,8 @@ static void widgetTreeViewDestroy(WidgetT *w) {
// ============================================================ // ============================================================
static void widgetTreeItemDestroy(WidgetT *w) { static void widgetTreeItemDestroy(WidgetT *w) {
TreeItemDataT *ti = (TreeItemDataT *)w->data;
free((void *)ti->text);
free(w->data); free(w->data);
w->data = NULL; w->data = NULL;
} }
@ -1655,7 +1658,7 @@ WidgetT *wgtTreeItem(WidgetT *parent, const char *text) {
if (ti) { if (ti) {
w->data = ti; w->data = ti;
ti->text = text; ti->text = text ? strdup(text) : NULL;
ti->expanded = false; ti->expanded = false;
} }