An insane number of performance, logic, and feature enhancements; bug fixess; and other things.
This commit is contained in:
parent
3c886a97f6
commit
454a3620f7
53 changed files with 3355 additions and 858 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@ dosbench/
|
|||
bin/
|
||||
obj/
|
||||
lib/
|
||||
*~
|
||||
*.~
|
||||
.gitignore~
|
||||
.gitattributes~
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ OBJDIR = ../../obj/dvxbasic
|
|||
LIBSDIR = ../../bin/libs
|
||||
APPDIR = ../../bin/apps/kpunch/dvxbasic
|
||||
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)
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -85,8 +84,6 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR)
|
|||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $(COMP_OBJS) $(APP_OBJS)
|
||||
$(DVXRES) build $@ dvxbasic.res
|
||||
|
||||
install-samples: $(SAMPLES) | $(APPDIR)
|
||||
cp $(SAMPLES) $(APPDIR)/
|
||||
|
||||
# Object files
|
||||
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -689,7 +689,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) {
|
|||
|
||||
// If it's a keyword and has no suffix, return the keyword token.
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ static void advance(BasParserT *p) {
|
|||
if (p->hasError) {
|
||||
return;
|
||||
}
|
||||
p->prevLine = p->lex.token.line;
|
||||
basLexerNext(&p->lex);
|
||||
if (p->lex.token.type == TOK_ERROR) {
|
||||
error(p, p->lex.error);
|
||||
|
|
@ -345,8 +346,16 @@ static void error(BasParserT *p, const char *msg) {
|
|||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Not a UDT -- treat as control property read: CtrlName.Property
|
||||
// Not a UDT -- treat as control property/method: CtrlName.Member
|
||||
advance(p); // consume DOT
|
||||
if (!check(p, TOK_IDENT)) {
|
||||
errorExpected(p, "property name");
|
||||
if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') {
|
||||
errorExpected(p, "property or method name");
|
||||
return;
|
||||
}
|
||||
char memberName[BAS_MAX_TOKEN_LEN];
|
||||
|
|
@ -1758,11 +1767,32 @@ static void parsePrimary(BasParserT *p) {
|
|||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
|
||||
// Push property name, LOAD_PROP
|
||||
// If followed by '(', this is a method call with args
|
||||
if (check(p, TOK_LPAREN)) {
|
||||
advance(p); // consume '('
|
||||
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1895,7 +1925,9 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
// Emit: push current form ref, push ctrl name, FIND_CTRL
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
|
@ -2027,7 +2059,7 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
basEmit8(&p->cg, OP_FIND_CTRL_IDX);
|
||||
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
|
@ -5023,7 +5055,7 @@ static void parseStatement(BasParserT *p) {
|
|||
}
|
||||
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.");
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ typedef struct {
|
|||
char error[1024];
|
||||
bool hasError;
|
||||
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 optionBase; // default array lower bound (0 or 1)
|
||||
bool optionCompareText; // true = case-insensitive string comparison
|
||||
|
|
|
|||
|
|
@ -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--) {
|
||||
if (tab->symbols[i].formScopeEnded) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
// Search global scope
|
||||
return basSymTabFindGlobal(tab, name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -35,6 +35,7 @@ typedef struct BasControlT BasControlT;
|
|||
typedef struct {
|
||||
int32_t id;
|
||||
char name[BAS_MAX_CTRL_NAME];
|
||||
BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored)
|
||||
} BasMenuIdMapT;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -53,6 +54,7 @@ typedef struct BasControlT {
|
|||
char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text
|
||||
char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding
|
||||
char dataField[BAS_MAX_CTRL_NAME]; // column name for binding
|
||||
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
|
||||
} BasControlT;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -65,7 +67,7 @@ typedef struct BasFormT {
|
|||
WidgetT *root; // widget root (from wgtInitWindow)
|
||||
WidgetT *contentBox; // VBox/HBox for user controls
|
||||
AppContextT *ctx; // DVX app context
|
||||
BasControlT *controls; // stb_ds dynamic array
|
||||
BasControlT **controls; // stb_ds array of heap-allocated pointers
|
||||
int32_t controlCount;
|
||||
BasVmT *vm; // VM for event dispatch
|
||||
BasModuleT *module; // compiled module (for SUB lookup)
|
||||
|
|
@ -78,13 +80,16 @@ typedef struct BasFormT {
|
|||
bool frmHasResizable; // true if Resizable was explicitly set
|
||||
bool frmCentered;
|
||||
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)
|
||||
BasValueT *formVars;
|
||||
int32_t formVarCount;
|
||||
// Menu ID to name mapping (for event dispatch)
|
||||
BasMenuIdMapT *menuIdMap;
|
||||
int32_t menuIdMapCount;
|
||||
// Synthetic control entry for the form itself, so that
|
||||
// FormName.Property works through the same getProp/setProp path.
|
||||
BasControlT formCtrl;
|
||||
} BasFormT;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -102,7 +107,7 @@ typedef struct {
|
|||
AppContextT *ctx; // DVX app context
|
||||
BasVmT *vm; // shared VM instance
|
||||
BasModuleT *module; // compiled module
|
||||
BasFormT *forms; // stb_ds dynamic array
|
||||
BasFormT **forms; // stb_ds array of heap-allocated pointers
|
||||
int32_t formCount;
|
||||
BasFormT *currentForm; // form currently dispatching events
|
||||
BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
@ -151,7 +229,15 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
|
|||
} else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
|
||||
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
|
||||
} 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;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
if (strncasecmp(ds->form->controls[i].name, prefix, prefixLen) == 0) {
|
||||
int32_t num = atoi(ds->form->controls[i].name + prefixLen);
|
||||
if (strncasecmp(ds->form->controls[i]->name, prefix, prefixLen) == 0) {
|
||||
int32_t num = atoi(ds->form->controls[i]->name + prefixLen);
|
||||
|
||||
if (num > highest) {
|
||||
highest = num;
|
||||
|
|
@ -209,20 +295,36 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
|
|||
// then parent children inside their containers.
|
||||
// Pass 1: create all widgets as top-level children
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
DsgnControlT *ctrl = &ds->form->controls[i];
|
||||
DsgnControlT *ctrl = ds->form->controls[i];
|
||||
|
||||
if (ctrl->widget) {
|
||||
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;
|
||||
|
||||
if (ctrl->parentName[0]) {
|
||||
for (int32_t j = 0; j < count; j++) {
|
||||
if (j != i && ds->form->controls[j].widget &&
|
||||
strcasecmp(ds->form->controls[j].name, ctrl->parentName) == 0) {
|
||||
parent = ds->form->controls[j].widget;
|
||||
if (j != i && ds->form->controls[j]->widget &&
|
||||
strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -350,6 +452,9 @@ const char *dsgnDefaultEvent(const char *typeName) {
|
|||
|
||||
void dsgnFree(DsgnStateT *ds) {
|
||||
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->menuItems);
|
||||
free(ds->form->code);
|
||||
|
|
@ -407,6 +512,8 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
bool inForm = false;
|
||||
bool inMenu = false;
|
||||
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)
|
||||
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
|
||||
menuNestDepth++;
|
||||
inMenu = true;
|
||||
if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; }
|
||||
blockDepth++;
|
||||
} else if (inForm) {
|
||||
DsgnControlT ctrl;
|
||||
memset(&ctrl, 0, sizeof(ctrl));
|
||||
ctrl.index = -1;
|
||||
snprintf(ctrl.name, DSGN_MAX_NAME, "%s", ctrlName);
|
||||
snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName);
|
||||
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
|
||||
cp->index = -1;
|
||||
snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName);
|
||||
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
|
||||
|
||||
// Set parent from current nesting
|
||||
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;
|
||||
ctrl.height = DEFAULT_CTRL_H;
|
||||
arrput(form->controls, ctrl);
|
||||
curCtrl = &form->controls[arrlen(form->controls) - 1];
|
||||
cp->width = DEFAULT_CTRL_W;
|
||||
cp->height = DEFAULT_CTRL_H;
|
||||
arrput(form->controls, cp);
|
||||
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 (dsgnIsContainer(typeName) && nestDepth < 7) {
|
||||
if (isCtrl && nestDepth < 7) {
|
||||
snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName);
|
||||
nestDepth++;
|
||||
}
|
||||
|
|
@ -535,6 +646,9 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
}
|
||||
|
||||
if (strcasecmp(trimmed, "End") == 0) {
|
||||
if (blockDepth > 0) {
|
||||
blockDepth--;
|
||||
|
||||
if (inMenu) {
|
||||
menuNestDepth--;
|
||||
curMenuItem = NULL;
|
||||
|
|
@ -543,14 +657,16 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
menuNestDepth = 0;
|
||||
inMenu = false;
|
||||
}
|
||||
} else if (curCtrl) {
|
||||
} else {
|
||||
// If we're closing a container, pop the parent stack
|
||||
if (nestDepth > 0 && strcasecmp(parentStack[nestDepth - 1], curCtrl->name) == 0) {
|
||||
if (blockDepth < DSGN_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) {
|
||||
nestDepth--;
|
||||
}
|
||||
|
||||
curCtrl = NULL;
|
||||
}
|
||||
} else {
|
||||
// blockDepth == 0: this is the form's closing End
|
||||
inForm = false;
|
||||
|
||||
// Everything after the form's closing End is code
|
||||
|
|
@ -621,6 +737,7 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
if (curMenuItem) {
|
||||
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, "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) {
|
||||
if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); }
|
||||
|
|
@ -693,11 +810,12 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
|
|||
|
||||
// Delete key -- remove the selected control and any children
|
||||
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)
|
||||
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);
|
||||
|
||||
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);
|
||||
ds->selectedIdx = -1;
|
||||
ds->form->dirty = true;
|
||||
|
|
@ -728,7 +847,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
|
|||
|
||||
if (drag) {
|
||||
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 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) {
|
||||
// Determine if we should swap with a neighbor based on drag direction
|
||||
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) {
|
||||
// Dragging down -- swap with next control if past its midpoint
|
||||
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) {
|
||||
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 + 1] = tmp;
|
||||
rebuildWidgets(ds);
|
||||
|
|
@ -775,10 +894,10 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
|
|||
} else if (dy < 0 && ctrl->widget) {
|
||||
// Dragging up -- swap with previous control if past its midpoint
|
||||
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) {
|
||||
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 - 1] = tmp;
|
||||
rebuildWidgets(ds);
|
||||
|
|
@ -803,10 +922,10 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
|
|||
if (ds->activeTool[0] == '\0') {
|
||||
// Check grab handles of selected control first
|
||||
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) {
|
||||
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx];
|
||||
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
||||
ds->mode = DSGN_RESIZING;
|
||||
ds->activeHandle = handle;
|
||||
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
|
||||
const char *typeName = ds->activeTool;
|
||||
DsgnControlT ctrl;
|
||||
memset(&ctrl, 0, sizeof(ctrl));
|
||||
ctrl.index = -1;
|
||||
dsgnAutoName(ds, typeName, ctrl.name, DSGN_MAX_NAME);
|
||||
snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName);
|
||||
ctrl.width = DEFAULT_CTRL_W;
|
||||
ctrl.height = DEFAULT_CTRL_H;
|
||||
setPropValue(&ctrl, "Caption", ctrl.name);
|
||||
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
|
||||
cp->index = -1;
|
||||
dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME);
|
||||
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
|
||||
cp->width = DEFAULT_CTRL_W;
|
||||
cp->height = DEFAULT_CTRL_H;
|
||||
setPropValue(cp, "Caption", cp->name);
|
||||
|
||||
// Determine parent: if click is inside a container, nest there
|
||||
WidgetT *parentWidget = ds->form->contentBox;
|
||||
|
||||
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)) {
|
||||
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;
|
||||
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
|
@ -864,19 +982,19 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
|
|||
|
||||
// Create the live widget
|
||||
if (parentWidget) {
|
||||
ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget);
|
||||
cp->widget = dsgnCreateDesignWidget(typeName, parentWidget);
|
||||
|
||||
if (ctrl.widget) {
|
||||
ctrl.widget->minW = wgtPixels(ctrl.width);
|
||||
ctrl.widget->minH = wgtPixels(ctrl.height);
|
||||
if (cp->widget) {
|
||||
cp->widget->minW = wgtPixels(cp->width);
|
||||
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;
|
||||
|
||||
// Set text AFTER arrput so pointers into the array element are stable
|
||||
DsgnControlT *stable = &ds->form->controls[ds->selectedIdx];
|
||||
// Set text AFTER arrput so pointers are stable (heap-allocated, so always stable)
|
||||
DsgnControlT *stable = ds->form->controls[ds->selectedIdx];
|
||||
|
||||
if (stable->widget) {
|
||||
const char *caption = getPropValue(stable, "Caption");
|
||||
|
|
@ -931,7 +1049,7 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
|
|||
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) {
|
||||
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);
|
||||
|
||||
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
|
||||
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");
|
||||
}
|
||||
|
||||
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) {
|
||||
for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
|
||||
buf[pos++] = ' ';
|
||||
|
|
@ -1226,7 +1352,7 @@ const char *dsgnSelectedName(const DsgnStateT *ds) {
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
@ -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);
|
||||
|
||||
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) {
|
||||
continue;
|
||||
|
|
@ -1375,7 +1501,7 @@ static void rebuildWidgets(DsgnStateT *ds) {
|
|||
int32_t count = (int32_t)arrlen(ds->form->controls);
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@
|
|||
#define DSGN_MAX_NAME 32
|
||||
#define DSGN_MAX_TEXT 256
|
||||
#define DSGN_MAX_PROPS 32
|
||||
#define DSGN_MAX_FRM_NESTING 16
|
||||
#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)
|
||||
|
|
@ -44,6 +46,7 @@ typedef struct {
|
|||
char name[DSGN_MAX_NAME]; // "mnuFile"
|
||||
int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu
|
||||
bool checked;
|
||||
bool radioCheck; // true = radio bullet instead of checkmark
|
||||
bool enabled; // default true
|
||||
} DsgnMenuItemT;
|
||||
|
||||
|
|
@ -83,7 +86,7 @@ typedef struct {
|
|||
bool centered; // true = center on screen, false = use left/top
|
||||
bool autoSize; // true = dvxFitWindow, false = use width/height
|
||||
bool resizable; // true = user can resize at runtime
|
||||
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)
|
||||
bool dirty;
|
||||
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.
|
||||
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)
|
||||
// ============================================================
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -47,6 +47,7 @@ typedef struct {
|
|||
WidgetT *captionInput;
|
||||
WidgetT *nameInput;
|
||||
WidgetT *checkedCb;
|
||||
WidgetT *radioCheckCb;
|
||||
WidgetT *enabledCb;
|
||||
WidgetT *listBox;
|
||||
} MnuEdStateT;
|
||||
|
|
@ -144,6 +145,7 @@ static void applyFields(void) {
|
|||
}
|
||||
|
||||
mi->checked = wgtCheckboxIsChecked(sMed.checkedCb);
|
||||
mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb);
|
||||
mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb);
|
||||
}
|
||||
|
||||
|
|
@ -180,8 +182,9 @@ static void loadFields(void) {
|
|||
wgtSetText(sMed.captionInput, "");
|
||||
wgtSetText(sMed.nameInput, "");
|
||||
wgtCheckboxSetChecked(sMed.checkedCb, false);
|
||||
wgtCheckboxSetChecked(sMed.radioCheckCb, false);
|
||||
wgtCheckboxSetChecked(sMed.enabledCb, true);
|
||||
sMed.nameAutoGen = true; // new blank item — auto-gen eligible
|
||||
sMed.nameAutoGen = true; // new blank item -- auto-gen eligible
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +193,7 @@ static void loadFields(void) {
|
|||
wgtSetText(sMed.captionInput, mi->caption);
|
||||
wgtSetText(sMed.nameInput, mi->name);
|
||||
wgtCheckboxSetChecked(sMed.checkedCb, mi->checked);
|
||||
wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck);
|
||||
wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled);
|
||||
}
|
||||
|
||||
|
|
@ -662,13 +666,14 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) {
|
|||
WidgetT *chkRow = wgtHBox(root);
|
||||
chkRow->spacing = wgtPixels(12);
|
||||
sMed.checkedCb = wgtCheckbox(chkRow, "Checked");
|
||||
sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck");
|
||||
sMed.enabledCb = wgtCheckbox(chkRow, "Enabled");
|
||||
wgtCheckboxSetChecked(sMed.enabledCb, true);
|
||||
|
||||
// Listbox
|
||||
sMed.listBox = wgtListBox(root);
|
||||
sMed.listBox->weight = 100;
|
||||
sMed.listBox->onClick = onListClick;
|
||||
sMed.listBox->onChange = onListClick;
|
||||
|
||||
// Arrow buttons
|
||||
WidgetT *arrowRow = wgtHBox(root);
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], in
|
|||
static void onPrpClose(WindowT *win);
|
||||
static void onPropDblClick(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);
|
||||
|
||||
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) {
|
||||
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_ENUM WGT_IFACE_ENUM
|
||||
#define PROP_TYPE_READONLY 255
|
||||
#define PROP_TYPE_LAYOUT 251
|
||||
#define PROP_TYPE_DATASOURCE 254
|
||||
#define PROP_TYPE_DATAFIELD 253
|
||||
#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, "Visible") == 0) { return PROP_TYPE_BOOL; }
|
||||
if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; }
|
||||
if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; }
|
||||
if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; }
|
||||
if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; }
|
||||
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);
|
||||
|
||||
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) {
|
||||
continue;
|
||||
|
|
@ -421,40 +423,78 @@ static void onPropDblClick(WidgetT *w) {
|
|||
const char *propName = sCellData[row * 2];
|
||||
const char *curValue = sCellData[row * 2 + 1];
|
||||
|
||||
// Layout toggles directly -- no input box needed
|
||||
if (strcasecmp(propName, "Layout") == 0 && sDs->selectedIdx < 0) {
|
||||
if (strcasecmp(sDs->form->layout, "VBox") == 0) {
|
||||
snprintf(sDs->form->layout, DSGN_MAX_NAME, "HBox");
|
||||
} else {
|
||||
snprintf(sDs->form->layout, DSGN_MAX_NAME, "VBox");
|
||||
// Layout — select from discovered layout containers
|
||||
if (strcasecmp(propName, "Layout") == 0) {
|
||||
// Discover available layout types from loaded widget interfaces.
|
||||
// A layout container is isContainer with WGT_CREATE_PARENT (no extra args).
|
||||
const char *layoutNames[32];
|
||||
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;
|
||||
|
||||
// Replace the content box with the new layout type
|
||||
// Rebuild the form designer to apply the new layout
|
||||
if (sDs->formWin && sDs->formWin->widgetRoot) {
|
||||
WidgetT *root = sDs->formWin->widgetRoot;
|
||||
|
||||
// Remove old content box
|
||||
root->firstChild = NULL;
|
||||
root->lastChild = NULL;
|
||||
|
||||
// Create new content box
|
||||
WidgetT *contentBox;
|
||||
WidgetT *contentBox = dsgnCreateContentBox(root, layoutField);
|
||||
|
||||
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);
|
||||
|
||||
for (int32_t ci = 0; ci < cc; ci++) {
|
||||
sDs->form->controls[ci].widget = NULL;
|
||||
sDs->form->controls[ci]->widget = NULL;
|
||||
}
|
||||
|
||||
dsgnCreateWidgets(sDs, contentBox);
|
||||
|
|
@ -473,7 +513,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
int32_t ctrlCount = (int32_t)arrlen(sDs->form->controls);
|
||||
|
||||
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);
|
||||
|
|
@ -523,8 +563,8 @@ static void onPropDblClick(WidgetT *w) {
|
|||
dataNames[dataCount++] = "(none)";
|
||||
|
||||
for (int32_t i = 0; i < formCtrlCount && dataCount < 16; i++) {
|
||||
if (strcasecmp(sDs->form->controls[i].typeName, "Data") == 0) {
|
||||
dataNames[dataCount++] = sDs->form->controls[i].name;
|
||||
if (strcasecmp(sDs->form->controls[i]->typeName, "Data") == 0) {
|
||||
dataNames[dataCount++] = sDs->form->controls[i]->name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -561,7 +601,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
const char *dataSrc = "";
|
||||
|
||||
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) {
|
||||
dataSrc = selCtrl->name;
|
||||
|
|
@ -623,7 +663,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
const char *dbName = "";
|
||||
|
||||
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++) {
|
||||
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);
|
||||
|
||||
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) {
|
||||
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
|
||||
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) {
|
||||
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
|
||||
if (strcasecmp(ctrl->typeName, "Data") == 0) {
|
||||
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++) {
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
if (sDs->formWin) {
|
||||
|
|
@ -1022,7 +1062,7 @@ static void onTreeItemClick(WidgetT *w) {
|
|||
|
||||
// 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) {
|
||||
const char *label = (const char *)item->userData;
|
||||
|
||||
|
|
@ -1041,10 +1081,9 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcC
|
|||
itemName[ni] = '\0';
|
||||
|
||||
for (int32_t i = 0; i < srcCount; i++) {
|
||||
if (strcmp(srcArr[i].name, itemName) == 0) {
|
||||
DsgnControlT ctrl = srcArr[i];
|
||||
snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentName);
|
||||
arrput(*outArr, ctrl);
|
||||
if (strcmp(srcArr[i]->name, itemName) == 0) {
|
||||
snprintf(srcArr[i]->parentName, DSGN_MAX_NAME, "%s", parentName);
|
||||
arrput(*outArr, srcArr[i]);
|
||||
|
||||
// Recurse into children (for containers)
|
||||
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;
|
||||
|
||||
if (!sDs || !sDs->form || !sTree || sUpdating) {
|
||||
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);
|
||||
DsgnControlT *newArr = NULL;
|
||||
DsgnControlT **newArr = NULL;
|
||||
WidgetT *formItem = sTree->firstChild;
|
||||
|
||||
if (!formItem) {
|
||||
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, "");
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
|
|
@ -1138,7 +1214,7 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
|
|||
|
||||
// Control tree (top pane)
|
||||
sTree = wgtTreeView(splitter);
|
||||
sTree->onChange = onTreeReorder;
|
||||
sTree->onChange = onTreeChange;
|
||||
wgtTreeViewSetReorderable(sTree, true);
|
||||
|
||||
// Property ListView (bottom pane)
|
||||
|
|
@ -1217,7 +1293,7 @@ void prpRebuildTree(DsgnStateT *ds) {
|
|||
WidgetT **treeItems = NULL;
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
DsgnControlT *ctrl = &ds->form->controls[i];
|
||||
DsgnControlT *ctrl = ds->form->controls[i];
|
||||
char buf[128];
|
||||
|
||||
if (ctrl->index >= 0) {
|
||||
|
|
@ -1234,7 +1310,7 @@ void prpRebuildTree(DsgnStateT *ds) {
|
|||
|
||||
if (ctrl->parentName[0]) {
|
||||
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];
|
||||
break;
|
||||
}
|
||||
|
|
@ -1265,13 +1341,62 @@ void prpRebuildTree(DsgnStateT *ds) {
|
|||
// 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) {
|
||||
if (!ds || !ds->form) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't rebuild the tree here -- just update selection on existing items.
|
||||
// prpRebuildTree destroys all items which loses TreeView selection state.
|
||||
// Sync tree selection to match selectedIdx
|
||||
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
|
||||
if (!sPropList) {
|
||||
|
|
@ -1283,7 +1408,7 @@ void prpRefresh(DsgnStateT *ds) {
|
|||
int32_t count = (int32_t)arrlen(ds->form->controls);
|
||||
|
||||
if (ds->selectedIdx >= 0 && ds->selectedIdx < count) {
|
||||
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx];
|
||||
DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
|
||||
char buf[32];
|
||||
|
||||
addPropRow("Name", ctrl->name);
|
||||
|
|
|
|||
|
|
@ -429,8 +429,12 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
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];
|
||||
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
|
||||
struct stat st;
|
||||
|
|
@ -440,21 +444,21 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
continue;
|
||||
}
|
||||
|
||||
int32_t len = strlen(ent->d_name);
|
||||
int32_t len = strlen(name);
|
||||
|
||||
if (len < 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip ourselves
|
||||
if (strcasecmp(ent->d_name, "progman.app") == 0) {
|
||||
if (strcasecmp(name, "progman.app") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +473,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
nameLen = SHELL_APP_NAME_MAX - 1;
|
||||
}
|
||||
|
||||
memcpy(newEntry.name, ent->d_name, nameLen);
|
||||
memcpy(newEntry.name, name, nameLen);
|
||||
newEntry.name[nameLen] = '\0';
|
||||
|
||||
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);
|
||||
|
||||
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, "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);
|
||||
sAppCount = (int32_t)arrlen(sAppFiles);
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ TARGETDIR = $(LIBSDIR)/kpunch/libdvx
|
|||
TARGET = $(TARGETDIR)/libdvx.lib
|
||||
|
||||
# libdvx.lib export prefixes
|
||||
DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _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 _widget \
|
||||
-E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \
|
||||
|
|
|
|||
|
|
@ -5069,8 +5069,7 @@ bool dvxUpdate(AppContextT *ctx) {
|
|||
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
||||
WindowT *win = ctx->stack.windows[i];
|
||||
|
||||
if (win->needsPaint && win->onPaint) {
|
||||
win->needsPaint = false;
|
||||
if (win->fullRepaint && win->onPaint) {
|
||||
dvxInvalidateWindow(ctx, win);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -512,7 +512,7 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
|
|||
widgetLayoutChildren(root, &ctx->font);
|
||||
|
||||
// Paint widgets
|
||||
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
|
||||
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -507,7 +507,7 @@ typedef struct WindowT {
|
|||
bool resizable;
|
||||
bool modal;
|
||||
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 maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height)
|
||||
// Pre-maximize geometry is saved so wmRestore() can put the window
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ void videoShutdown(DisplayT *d);
|
|||
// linear scan of the grey ramp and chrome entries).
|
||||
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
|
||||
// will be clipped to this rectangle. The caller is responsible for
|
||||
// saving and restoring the clip rect around scoped operations.
|
||||
|
|
|
|||
|
|
@ -248,8 +248,15 @@ typedef struct WidgetT {
|
|||
bool enabled;
|
||||
bool readOnly;
|
||||
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
|
||||
|
||||
// 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
|
||||
// central event dispatcher, not from individual widget handlers.
|
||||
// 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.
|
||||
// Overlays (dropdown popups, tooltips) are painted in a second pass
|
||||
// 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
|
||||
|
|
@ -568,6 +575,13 @@ const void *wgtGetApi(const char *name);
|
|||
#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_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
|
||||
typedef struct {
|
||||
|
|
|
|||
37
core/dvxWm.c
37
core/dvxWm.c
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
@ -1291,7 +1323,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
|
|||
memset(win->contentBuf, 0xFF, bufSize);
|
||||
}
|
||||
|
||||
win->needsPaint = true;
|
||||
win->fullRepaint = true;
|
||||
|
||||
stack->windows[stack->count] = win;
|
||||
stack->count++;
|
||||
|
|
@ -1850,6 +1882,7 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
|
|||
}
|
||||
|
||||
if (win->onPaint) {
|
||||
win->fullRepaint = true;
|
||||
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
||||
win->onPaint(win, &fullRect);
|
||||
win->contentDirty = true;
|
||||
|
|
@ -2367,6 +2400,7 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_
|
|||
}
|
||||
|
||||
if (win->onPaint) {
|
||||
win->fullRepaint = true;
|
||||
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
||||
win->onPaint(win, &fullRect);
|
||||
win->contentDirty = true;
|
||||
|
|
@ -2420,6 +2454,7 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *
|
|||
}
|
||||
|
||||
if (win->onPaint) {
|
||||
win->fullRepaint = true;
|
||||
RectT fullRect = {0, 0, win->contentW, win->contentH};
|
||||
win->onPaint(win, &fullRect);
|
||||
win->contentDirty = true;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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.
|
||||
// Searches all menus in the menu bar. For radio items, setting
|
||||
// checked=true also unchecks other radio items in the same group.
|
||||
|
|
|
|||
|
|
@ -90,17 +90,8 @@ static void sysInfoAppend(const char *fmt, ...);
|
|||
static bool sHasMouseWheel = false;
|
||||
static int32_t sLastWheelDelta = 0;
|
||||
|
||||
// Software cursor tracking. Two modes detected at runtime:
|
||||
// Mickey mode (default): function 0Bh raw deltas accumulated into
|
||||
// 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;
|
||||
// Mouse coordinate range set by functions 07h/08h in platformMouseInit.
|
||||
// Position read directly from function 03h (absolute coordinates).
|
||||
static int32_t sMouseRangeW = 0;
|
||||
static int32_t sMouseRangeH = 0;
|
||||
static int32_t sCurX = 0;
|
||||
|
|
@ -1564,13 +1555,11 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
|
|||
sCurX = screenW / 2;
|
||||
sCurY = screenH / 2;
|
||||
|
||||
// Function 00h: reset driver, detect mouse hardware
|
||||
// Function 00h: reset driver
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.x.ax = 0x0000;
|
||||
__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
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.x.ax = 0x0007;
|
||||
|
|
@ -1592,10 +1581,6 @@ void platformMouseInit(int32_t screenW, int32_t screenH) {
|
|||
r.x.dx = screenH / 2;
|
||||
__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
|
||||
// ============================================================
|
||||
//
|
||||
// Reads button state via function 03h and raw mickey deltas via
|
||||
// function 0Bh. Position is tracked in software (sCurX/sCurY)
|
||||
// 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.
|
||||
// Reads button state and cursor position via INT 33h function 03h.
|
||||
// Coordinate range was set by functions 07h/08h in platformMouseInit.
|
||||
|
||||
void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
|
||||
__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);
|
||||
}
|
||||
|
||||
int32_t absX = (int16_t)r.x.cx;
|
||||
int32_t absY = (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;
|
||||
}
|
||||
sCurX = (int16_t)r.x.cx;
|
||||
sCurY = (int16_t)r.x.dx;
|
||||
|
||||
if (sCurX < 0) {
|
||||
sCurX = 0;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
.topic sys.overview
|
||||
.title DVX System Overview
|
||||
.toc 0 System Overview
|
||||
.topic sys.welcome
|
||||
.title Welcome!
|
||||
.toc 0 Welcome!
|
||||
.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.
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
|
|||
if (sDragWidget && !(buttons & MOUSE_LEFT)) {
|
||||
wclsOnDragEnd(sDragWidget, root, x, y);
|
||||
|
||||
wgtInvalidatePaint(root);
|
||||
wgtInvalidatePaint(sDragWidget);
|
||||
sDragWidget = NULL;
|
||||
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)) {
|
||||
wgtInvalidate(sDragWidget);
|
||||
} else {
|
||||
wgtInvalidatePaint(root);
|
||||
wgtInvalidatePaint(sDragWidget);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -353,6 +353,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
|
|||
}
|
||||
|
||||
if (!(buttons & MOUSE_LEFT)) {
|
||||
sPrevMouseButtons = 0;
|
||||
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) {
|
||||
int32_t relX = vx - hit->x;
|
||||
int32_t relY = vy - hit->y;
|
||||
int32_t relX = vx - hit->x - hit->contentOffX;
|
||||
int32_t relY = vy - hit->y - hit->contentOffY;
|
||||
|
||||
// MouseDown: button just pressed
|
||||
if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) {
|
||||
|
|
@ -518,8 +519,13 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
|||
cd.clipW = win->contentW;
|
||||
cd.clipH = win->contentH;
|
||||
|
||||
// Clear background
|
||||
bool full = win->fullRepaint;
|
||||
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
|
||||
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;
|
||||
|
|
@ -531,7 +537,10 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
|||
root->y = -scrollY;
|
||||
root->w = layoutW;
|
||||
root->h = layoutH;
|
||||
|
||||
if (full) {
|
||||
widgetLayoutChildren(root, &ctx->font);
|
||||
}
|
||||
|
||||
// Auto-focus first focusable widget if nothing has focus yet
|
||||
if (!sFocusedWidget) {
|
||||
|
|
@ -542,8 +551,8 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
|
|||
}
|
||||
}
|
||||
|
||||
// Paint widget tree (clip rect limits drawing to visible area)
|
||||
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
|
||||
// Paint widget tree (full = all widgets, partial = only dirty ones)
|
||||
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, full);
|
||||
|
||||
// Paint overlay popups (dropdown/combobox)
|
||||
widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
#include "dvxPlat.h"
|
||||
#include "../widgets/box/box.h"
|
||||
|
||||
static bool sFullRepaint = false;
|
||||
|
||||
|
||||
// ============================================================
|
||||
// debugContainerBorder
|
||||
|
|
@ -75,25 +77,32 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
|
|||
return;
|
||||
}
|
||||
|
||||
// Paint this widget via vtable
|
||||
// On partial repaints (fullRepaint=false), only paint dirty widgets.
|
||||
// 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
|
||||
if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
|
||||
if (sDebugLayout) {
|
||||
if (sDebugLayout && dirty) {
|
||||
debugContainerBorder(w, d, ops);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Paint children
|
||||
// Always recurse into children — a clean parent may have dirty children
|
||||
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
||||
widgetPaintOne(c, d, ops, font, colors);
|
||||
}
|
||||
|
||||
// Debug: draw container borders on top of children
|
||||
if (sDebugLayout && w->firstChild) {
|
||||
if (sDebugLayout && dirty && w->firstChild) {
|
||||
debugContainerBorder(w, d, ops);
|
||||
}
|
||||
}
|
||||
|
|
@ -355,7 +364,8 @@ void wgtInvalidate(WidgetT *w) {
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
@ -373,6 +383,9 @@ void wgtInvalidatePaint(WidgetT *w) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Mark only this widget as needing repaint
|
||||
w->paintDirty = true;
|
||||
|
||||
WidgetT *root = w;
|
||||
|
||||
while (root->parent) {
|
||||
|
|
@ -385,7 +398,7 @@ void wgtInvalidatePaint(WidgetT *w) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Dirty the window -- dvxInvalidateWindow calls onPaint automatically
|
||||
// Partial repaint — only dirty widgets will be repainted
|
||||
dvxInvalidateWindow(ctx, w->window);
|
||||
}
|
||||
|
||||
|
|
@ -394,12 +407,14 @@ void wgtInvalidatePaint(WidgetT *w) {
|
|||
// 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
sFullRepaint = fullRepaint;
|
||||
widgetPaintOne(root, d, ops, font, colors);
|
||||
sFullRepaint = false;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DVX System Overview</title>
|
||||
<title>Welcome!</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 0; padding: 0; display: flex; }
|
||||
nav { width: 250px; min-width: 250px; background: #f0f0f0; padding: 16px;
|
||||
|
|
@ -29,7 +29,7 @@ img { max-width: 100%; }
|
|||
<nav>
|
||||
<h3>Contents</h3>
|
||||
<ul>
|
||||
<li><a href="#sys.overview">System Overview</a></li>
|
||||
<li><a href="#sys.welcome">Welcome!</a></li>
|
||||
<li><strong>Architecture</strong>
|
||||
<ul>
|
||||
<li><a href="#arch.overview">System Overview</a>
|
||||
|
|
@ -761,9 +761,9 @@ img { max-width: 100%; }
|
|||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="topic" id="sys.overview">
|
||||
<h1>DVX System Overview</h1>
|
||||
<h2>DVX System Overview</h2>
|
||||
<div class="topic" id="sys.welcome">
|
||||
<h1>Welcome!</h1>
|
||||
<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>
|
||||
</div>
|
||||
<div class="topic" id="arch.overview">
|
||||
|
|
|
|||
37
dosbox-staging-overrides.conf
Normal file
37
dosbox-staging-overrides.conf
Normal 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
|
||||
|
|
@ -26,12 +26,15 @@ vesa oldvbe10 = false
|
|||
umb = true
|
||||
xms = true
|
||||
ems = true
|
||||
ver = 6.22
|
||||
lfn = false
|
||||
|
||||
[serial]
|
||||
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
|
||||
|
||||
[autoexec]
|
||||
mount d ~/dos
|
||||
mount c .
|
||||
c:
|
||||
cd bin
|
||||
dir
|
||||
dvx
|
||||
|
|
@ -23,12 +23,8 @@
|
|||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Route stb_ds allocations through the tracking wrappers so that
|
||||
// arrput/arrfree in DXE code is tracked per-app.
|
||||
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)
|
||||
// The loader is not a DXE — use plain realloc/free for stb_ds so that
|
||||
// all translation units (loaderMain.o, dvxPrefs.o) share the same heap.
|
||||
#define STB_DS_IMPLEMENTATION
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
|
|
@ -240,7 +236,9 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
|
|||
struct dirent *ent;
|
||||
|
||||
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 ..
|
||||
if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) {
|
||||
|
|
@ -516,8 +514,12 @@ static int32_t countHcfFilesRecurse(const char *dirPath) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Copy d_name before any recursion — readdir may use a shared buffer
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name);
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
|
||||
|
||||
struct stat st;
|
||||
|
||||
|
|
@ -528,9 +530,9 @@ static int32_t countHcfFilesRecurse(const char *dirPath) {
|
|||
if (S_ISDIR(st.st_mode)) {
|
||||
count += countHcfFilesRecurse(fullPath);
|
||||
} 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++;
|
||||
}
|
||||
}
|
||||
|
|
@ -585,8 +587,11 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
|
|||
continue;
|
||||
}
|
||||
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, ent->d_name);
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPart, DVX_PATH_SEP, name);
|
||||
|
||||
struct stat st;
|
||||
|
||||
|
|
@ -598,8 +603,8 @@ static void writeGlobToResp(FILE *resp, const char *pattern, const char *exclude
|
|||
char subPattern[DVX_MAX_PATH];
|
||||
snprintf(subPattern, sizeof(subPattern), "%s%c%s", fullPath, DVX_PATH_SEP, globPart);
|
||||
writeGlobToResp(resp, subPattern, excludePattern);
|
||||
} else if (platformGlobMatch(globPart, ent->d_name)) {
|
||||
if (excludePattern && platformGlobMatch(excludePattern, ent->d_name)) {
|
||||
} else if (platformGlobMatch(globPart, name)) {
|
||||
if (excludePattern && platformGlobMatch(excludePattern, name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -730,8 +735,12 @@ static void processHcfDir(const char *dirPath) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Copy d_name before any recursion — readdir may use a shared buffer
|
||||
char name[DVX_MAX_PATH];
|
||||
snprintf(name, sizeof(name), "%s", ent->d_name);
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name);
|
||||
snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name);
|
||||
|
||||
struct stat st;
|
||||
|
||||
|
|
@ -742,9 +751,9 @@ static void processHcfDir(const char *dirPath) {
|
|||
if (S_ISDIR(st.st_mode)) {
|
||||
processHcfDir(fullPath);
|
||||
} 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);
|
||||
sSplashLoaded++;
|
||||
splashUpdateProgress();
|
||||
|
|
|
|||
5
run.sh
5
run.sh
|
|
@ -1,2 +1,5 @@
|
|||
#!/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
|
||||
|
|
|
|||
17
sdk/samples/basic/iconed/iconed.dbp
Normal file
17
sdk/samples/basic/iconed/iconed.dbp
Normal 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
|
||||
476
sdk/samples/basic/iconed/iconed.frm
Normal file
476
sdk/samples/basic/iconed/iconed.frm
Normal 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
|
||||
|
|
@ -68,6 +68,8 @@ typedef struct {
|
|||
char args[1024]; // launch arguments (empty if none)
|
||||
char helpFile[DVX_MAX_PATH]; // help file path (for F1 context help)
|
||||
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;
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -106,7 +106,13 @@ static void f1HelpHandler(void *ctx) {
|
|||
if (focusedAppId > 0) {
|
||||
ShellAppT *app = shellGetApp(focusedAppId);
|
||||
|
||||
if (app && app->dxeCtx && app->dxeCtx->helpFile[0]) {
|
||||
if (app && app->dxeCtx) {
|
||||
// Let the app refresh helpTopic based on current context
|
||||
if (app->dxeCtx->onHelpQuery) {
|
||||
app->dxeCtx->onHelpQuery(app->dxeCtx->helpQueryCtx);
|
||||
}
|
||||
|
||||
if (app->dxeCtx->helpFile[0]) {
|
||||
if (app->dxeCtx->helpTopic[0]) {
|
||||
snprintf(args, sizeof(args), "%s %s",
|
||||
app->dxeCtx->helpFile, app->dxeCtx->helpTopic);
|
||||
|
|
@ -115,6 +121,7 @@ static void f1HelpHandler(void *ctx) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to system help if no app-specific help
|
||||
char viewerPath[DVX_MAX_PATH];
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@ void widgetFrameGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int3
|
|||
// ============================================================
|
||||
|
||||
void widgetFrameDestroy(WidgetT *w) {
|
||||
FrameDataT *fd = (FrameDataT *)w->data;
|
||||
free((void *)fd->title);
|
||||
free(w->data);
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +181,7 @@ WidgetT *wgtFrame(WidgetT *parent, const char *title) {
|
|||
|
||||
if (w) {
|
||||
FrameDataT *fd = calloc(1, sizeof(FrameDataT));
|
||||
fd->title = title;
|
||||
fd->title = title ? strdup(title) : NULL;
|
||||
fd->style = FrameInE;
|
||||
fd->color = 0;
|
||||
w->data = fd;
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|||
// ============================================================
|
||||
|
||||
static void widgetButtonDestroy(WidgetT *w) {
|
||||
ButtonDataT *d = (ButtonDataT *)w->data;
|
||||
free((void *)d->text);
|
||||
free(w->data);
|
||||
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) {
|
||||
ButtonDataT *d = (ButtonDataT *)w->data;
|
||||
d->text = text;
|
||||
free((void *)d->text);
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +245,7 @@ WidgetT *wgtButton(WidgetT *parent, const char *text) {
|
|||
if (w) {
|
||||
ButtonDataT *d = calloc(1, sizeof(ButtonDataT));
|
||||
w->data = d;
|
||||
d->text = text;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================
|
||||
|
|
@ -714,6 +777,9 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
|
|||
WidgetT *wgt = widgetAlloc(parent, sTypeId);
|
||||
|
||||
if (wgt) {
|
||||
wgt->contentOffX = CANVAS_BORDER;
|
||||
wgt->contentOffY = CANVAS_BORDER;
|
||||
|
||||
CanvasDataT *cd = (CanvasDataT *)calloc(1, sizeof(CanvasDataT));
|
||||
|
||||
cd->pixelData = data;
|
||||
|
|
@ -722,7 +788,7 @@ WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) {
|
|||
cd->canvasPitch = pitch;
|
||||
cd->canvasBpp = bpp;
|
||||
cd->penColor = packColor(d, 0, 0, 0);
|
||||
cd->penSize = 2;
|
||||
cd->penSize = 1;
|
||||
cd->lastX = -1;
|
||||
cd->lastY = -1;
|
||||
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) {
|
||||
if (!w || w->type != sTypeId || !path) {
|
||||
return -1;
|
||||
|
|
@ -909,6 +1021,7 @@ static const struct {
|
|||
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);
|
||||
void (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text);
|
||||
void (*resize)(WidgetT *w, int32_t newW, int32_t newH);
|
||||
} sApi = {
|
||||
.create = wgtCanvas,
|
||||
.clear = wgtCanvasClear,
|
||||
|
|
@ -923,11 +1036,22 @@ static const struct {
|
|||
.fillCircle = wgtCanvasFillCircle,
|
||||
.setPixel = wgtCanvasSetPixel,
|
||||
.getPixel = wgtCanvasGetPixel,
|
||||
.drawText = wgtCanvasDrawText
|
||||
.drawText = wgtCanvasDrawText,
|
||||
.resize = wgtCanvasResize
|
||||
};
|
||||
|
||||
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 = {
|
||||
|
|
@ -935,7 +1059,7 @@ static const WgtIfaceT sIface = {
|
|||
.props = NULL,
|
||||
.propCount = 0,
|
||||
.methods = sMethods,
|
||||
.methodCount = 1,
|
||||
.methodCount = 11,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT_INT_INT,
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|||
// ============================================================
|
||||
|
||||
static void widgetCheckboxDestroy(WidgetT *w) {
|
||||
CheckboxDataT *d = (CheckboxDataT *)w->data;
|
||||
free((void *)d->text);
|
||||
free(w->data);
|
||||
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) {
|
||||
CheckboxDataT *d = (CheckboxDataT *)w->data;
|
||||
d->text = text;
|
||||
free((void *)d->text);
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +214,7 @@ WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
|
|||
if (w) {
|
||||
CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT));
|
||||
w->data = d;
|
||||
d->text = text;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ typedef struct {
|
|||
void (*setItems)(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t (*getSelected)(const WidgetT *w);
|
||||
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;
|
||||
|
||||
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 wgtComboBoxGetSelected(w) dvxComboBoxApi()->getSelected(w)
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
#include "dvxWgtP.h"
|
||||
#include "../texthelp/textHelp.h"
|
||||
#include "../listhelp/listHelp.h"
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
static int32_t sTypeId = -1;
|
||||
|
||||
|
|
@ -45,6 +46,9 @@ typedef struct {
|
|||
int32_t hoverIdx;
|
||||
int32_t listScrollPos;
|
||||
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;
|
||||
|
||||
|
||||
|
|
@ -74,6 +78,10 @@ void widgetComboBoxDestroy(WidgetT *w) {
|
|||
ComboBoxDataT *d = (ComboBoxDataT *)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->buf);
|
||||
free(d->undoBuf);
|
||||
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
|
||||
// ============================================================
|
||||
|
|
@ -530,23 +602,41 @@ static const struct {
|
|||
void (*setItems)(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t (*getSelected)(const WidgetT *w);
|
||||
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 = {
|
||||
.create = wgtComboBox,
|
||||
.setItems = wgtComboBoxSetItems,
|
||||
.getSelected = wgtComboBoxGetSelected,
|
||||
.setSelected = wgtComboBoxSetSelected
|
||||
.setSelected = wgtComboBoxSetSelected,
|
||||
.addItem = wgtComboBoxAddItem,
|
||||
.removeItem = wgtComboBoxRemoveItem,
|
||||
.clear = wgtComboBoxClear,
|
||||
.getItem = wgtComboBoxGetItem,
|
||||
.getItemCount = wgtComboBoxGetItemCount
|
||||
};
|
||||
|
||||
static const WgtPropDescT sProps[] = {
|
||||
{ "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 = {
|
||||
.basName = "ComboBox",
|
||||
.props = sProps,
|
||||
.propCount = 1,
|
||||
.methods = NULL,
|
||||
.methodCount = 0,
|
||||
.methods = sMethods,
|
||||
.methodCount = 5,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT_INT,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ typedef struct {
|
|||
void (*setItems)(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t (*getSelected)(const WidgetT *w);
|
||||
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;
|
||||
|
||||
static inline const DropdownApiT *dvxDropdownApi(void) {
|
||||
|
|
@ -21,5 +26,10 @@ static inline const DropdownApiT *dvxDropdownApi(void) {
|
|||
#define wgtDropdownSetItems(w, items, count) dvxDropdownApi()->setItems(w, items, count)
|
||||
#define wgtDropdownGetSelected(w) dvxDropdownApi()->getSelected(w)
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
#include "../texthelp/textHelp.h"
|
||||
#include "../listhelp/listHelp.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
static int32_t sTypeId = -1;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -33,6 +37,9 @@ typedef struct {
|
|||
int32_t hoverIdx;
|
||||
int32_t scrollPos;
|
||||
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;
|
||||
|
||||
|
||||
|
|
@ -41,7 +48,16 @@ typedef struct {
|
|||
// ============================================================
|
||||
|
||||
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
|
||||
// ============================================================
|
||||
|
|
@ -402,23 +501,41 @@ static const struct {
|
|||
void (*setItems)(WidgetT *w, const char **items, int32_t count);
|
||||
int32_t (*getSelected)(const WidgetT *w);
|
||||
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 = {
|
||||
.create = wgtDropdown,
|
||||
.setItems = wgtDropdownSetItems,
|
||||
.getSelected = wgtDropdownGetSelected,
|
||||
.setSelected = wgtDropdownSetSelected
|
||||
.setSelected = wgtDropdownSetSelected,
|
||||
.addItem = wgtDropdownAddItem,
|
||||
.removeItem = wgtDropdownRemoveItem,
|
||||
.clear = wgtDropdownClear,
|
||||
.getItem = wgtDropdownGetItem,
|
||||
.getItemCount = wgtDropdownGetItemCount
|
||||
};
|
||||
|
||||
static const WgtPropDescT sProps[] = {
|
||||
{ "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 = {
|
||||
.basName = "DropDown",
|
||||
.props = sProps,
|
||||
.propCount = 1,
|
||||
.methods = NULL,
|
||||
.methodCount = 0,
|
||||
.methods = sMethods,
|
||||
.methodCount = 5,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@
|
|||
// the Win3.1 convention where labels act as keyboard shortcuts for adjacent
|
||||
// controls (e.g., a label "&Name:" before a text input).
|
||||
//
|
||||
// The text pointer is stored directly (not copied) -- the caller must ensure
|
||||
// the string remains valid for the widget's lifetime, or use widgetLabelSetText
|
||||
// to update it. This avoids unnecessary allocations for the common case of
|
||||
// literal string labels.
|
||||
// The widget owns its text string (strdup'd on set, freed on destroy).
|
||||
// Callers can pass transient strings safely.
|
||||
//
|
||||
// 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
|
||||
|
|
@ -49,6 +47,8 @@ void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|||
// ============================================================
|
||||
|
||||
static void widgetLabelDestroy(WidgetT *w) {
|
||||
LabelDataT *d = (LabelDataT *)w->data;
|
||||
free((void *)d->text);
|
||||
free(w->data);
|
||||
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) {
|
||||
LabelDataT *d = (LabelDataT *)w->data;
|
||||
d->text = text;
|
||||
free((void *)d->text);
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +132,7 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text) {
|
|||
if (w) {
|
||||
LabelDataT *d = calloc(1, sizeof(LabelDataT));
|
||||
w->data = d;
|
||||
d->text = text;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ typedef struct {
|
|||
void (*selectAll)(WidgetT *w);
|
||||
void (*clearSelection)(WidgetT *w);
|
||||
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;
|
||||
|
||||
static inline const ListBoxApiT *dvxListBoxApi(void) {
|
||||
|
|
|
|||
|
|
@ -49,10 +49,14 @@ typedef struct {
|
|||
int32_t dropIdx;
|
||||
int32_t sbDragOrient;
|
||||
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;
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
#define LISTBOX_PAD 2
|
||||
#define LISTBOX_MIN_ROWS 4
|
||||
|
|
@ -136,6 +140,10 @@ void widgetListBoxDestroy(WidgetT *w) {
|
|||
ListBoxDataT *d = (ListBoxDataT *)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->selBits);
|
||||
free(d);
|
||||
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) {
|
||||
VALIDATE_WIDGET_VOID(w, sTypeId);
|
||||
|
||||
|
|
@ -844,6 +912,11 @@ static const struct {
|
|||
void (*selectAll)(WidgetT *w);
|
||||
void (*clearSelection)(WidgetT *w);
|
||||
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 = {
|
||||
.create = wgtListBox,
|
||||
.setItems = wgtListBoxSetItems,
|
||||
|
|
@ -854,7 +927,12 @@ static const struct {
|
|||
.setItemSelected = wgtListBoxSetItemSelected,
|
||||
.selectAll = wgtListBoxSelectAll,
|
||||
.clearSelection = wgtListBoxClearSelection,
|
||||
.setReorderable = wgtListBoxSetReorderable
|
||||
.setReorderable = wgtListBoxSetReorderable,
|
||||
.addItem = wgtListBoxAddItem,
|
||||
.removeItem = wgtListBoxRemoveItem,
|
||||
.clear = wgtListBoxClear,
|
||||
.getItem = wgtListBoxGetItem,
|
||||
.getItemCount = wgtListBoxGetItemCount
|
||||
};
|
||||
|
||||
static const WgtPropDescT sProps[] = {
|
||||
|
|
@ -862,12 +940,17 @@ static const WgtPropDescT sProps[] = {
|
|||
};
|
||||
|
||||
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 },
|
||||
{ "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 },
|
||||
{ "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 = {
|
||||
|
|
@ -875,7 +958,7 @@ static const WgtIfaceT sIface = {
|
|||
.props = sProps,
|
||||
.propCount = 1,
|
||||
.methods = sMethods,
|
||||
.methodCount = 6,
|
||||
.methodCount = 11,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ typedef struct {
|
|||
void (*selectAll)(WidgetT *w);
|
||||
void (*clearSelection)(WidgetT *w);
|
||||
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;
|
||||
|
||||
static inline const ListViewApiT *dvxListViewApi(void) {
|
||||
|
|
@ -43,5 +49,11 @@ static inline const ListViewApiT *dvxListViewApi(void) {
|
|||
#define wgtListViewSelectAll(w) dvxListViewApi()->selectAll(w)
|
||||
#define wgtListViewClearSelection(w) dvxListViewApi()->clearSelection(w)
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ static int32_t sTypeId = -1;
|
|||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
#define LISTVIEW_MAX_COLS 16
|
||||
#define LISTVIEW_PAD 3
|
||||
|
|
@ -96,6 +97,10 @@ typedef struct {
|
|||
bool resizeDragging;
|
||||
int32_t sbDragOrient;
|
||||
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;
|
||||
|
||||
|
||||
|
|
@ -105,6 +110,7 @@ typedef struct {
|
|||
|
||||
static void allocListViewSelBits(WidgetT *w);
|
||||
static void listViewBuildSortIndex(WidgetT *w);
|
||||
static void listViewSyncOwned(WidgetT *w);
|
||||
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);
|
||||
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
|
||||
// ============================================================
|
||||
|
|
@ -338,6 +365,10 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) {
|
|||
void widgetListViewDestroy(WidgetT *w) {
|
||||
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->sortIndex);
|
||||
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) {
|
||||
if (!w || w->type != sTypeId) {
|
||||
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) {
|
||||
VALIDATE_WIDGET(w, sTypeId, -1);
|
||||
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) {
|
||||
if (!w || w->type != sTypeId) {
|
||||
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)) {
|
||||
VALIDATE_WIDGET_VOID(w, sTypeId);
|
||||
ListViewDataT *lv = (ListViewDataT *)w->data;
|
||||
|
|
@ -1721,6 +1881,12 @@ static const struct {
|
|||
void (*selectAll)(WidgetT *w);
|
||||
void (*clearSelection)(WidgetT *w);
|
||||
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 = {
|
||||
.create = wgtListView,
|
||||
.setColumns = wgtListViewSetColumns,
|
||||
|
|
@ -1734,7 +1900,13 @@ static const struct {
|
|||
.setItemSelected = wgtListViewSetItemSelected,
|
||||
.selectAll = wgtListViewSelectAll,
|
||||
.clearSelection = wgtListViewClearSelection,
|
||||
.setReorderable = wgtListViewSetReorderable
|
||||
.setReorderable = wgtListViewSetReorderable,
|
||||
.addItem = wgtListViewAddItem,
|
||||
.removeRow = wgtListViewRemoveRow,
|
||||
.clear = wgtListViewClear,
|
||||
.getCell = wgtListViewGetCell,
|
||||
.setCell = wgtListViewSetCell,
|
||||
.getRowCount = wgtListViewGetRowCount
|
||||
};
|
||||
|
||||
static const WgtPropDescT sProps[] = {
|
||||
|
|
@ -1742,12 +1914,18 @@ static const WgtPropDescT sProps[] = {
|
|||
};
|
||||
|
||||
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 },
|
||||
{ "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 },
|
||||
{ "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 = {
|
||||
|
|
@ -1755,7 +1933,7 @@ static const WgtIfaceT sIface = {
|
|||
.props = sProps,
|
||||
.propCount = 1,
|
||||
.methods = sMethods,
|
||||
.methodCount = 6,
|
||||
.methodCount = 12,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|||
// ============================================================
|
||||
|
||||
static void widgetRadioDestroy(WidgetT *w) {
|
||||
RadioDataT *d = (RadioDataT *)w->data;
|
||||
free((void *)d->text);
|
||||
free(w->data);
|
||||
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) {
|
||||
RadioDataT *d = (RadioDataT *)w->data;
|
||||
d->text = text;
|
||||
free((void *)d->text);
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
|
|
@ -344,7 +347,7 @@ WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
|||
if (w) {
|
||||
RadioDataT *d = calloc(1, sizeof(RadioDataT));
|
||||
w->data = d;
|
||||
d->text = text;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
|
||||
// Auto-assign index based on position in parent
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ typedef struct {
|
|||
void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx);
|
||||
int32_t (*getCursorLine)(const WidgetT *w);
|
||||
void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t));
|
||||
int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize);
|
||||
} TextInputApiT;
|
||||
|
||||
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 wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w)
|
||||
#define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn)
|
||||
#define wgtTextAreaGetWordAtCursor(w, buf, sz) dvxTextInputApi()->getWordAtCursor(w, buf, sz)
|
||||
|
||||
#endif // TEXTINPT_H
|
||||
|
|
|
|||
|
|
@ -136,6 +136,15 @@ typedef struct {
|
|||
|
||||
// Gutter click callback (optional). Fired when user clicks in the gutter.
|
||||
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;
|
||||
|
||||
#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 void textAreaRebuildCache(WidgetT *w);
|
||||
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 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);
|
||||
|
|
@ -734,6 +744,12 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
|
|||
}
|
||||
|
||||
int32_t totalLines = textAreaGetLineCount(w);
|
||||
|
||||
// Return cached value if line count hasn't changed
|
||||
if (ta->cachedGutterW > 0 && ta->cachedGutterLines == totalLines) {
|
||||
return ta->cachedGutterW;
|
||||
}
|
||||
|
||||
int32_t digits = 1;
|
||||
int32_t temp = totalLines;
|
||||
|
||||
|
|
@ -746,7 +762,10 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) {
|
|||
digits = 3;
|
||||
}
|
||||
|
||||
return (digits + 1) * font->charWidth;
|
||||
ta->cachedGutterW = (digits + 1) * font->charWidth;
|
||||
ta->cachedGutterLines = totalLines;
|
||||
|
||||
return ta->cachedGutterW;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -824,7 +843,22 @@ static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) {
|
|||
|
||||
static int32_t textAreaGetMaxLineLen(WidgetT *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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -832,6 +866,144 @@ static inline void textAreaDirtyCache(WidgetT *w) {
|
|||
TextAreaDataT *ta = (TextAreaDataT *)w->data;
|
||||
ta->cachedLines = -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) {
|
||||
TextAreaDataT *ta = (TextAreaDataT *)w->data;
|
||||
int32_t row = ta->cursorRow;
|
||||
|
|
@ -957,6 +1156,9 @@ void widgetTextAreaDestroy(WidgetT *w) {
|
|||
free(ta->undoBuf);
|
||||
free(ta->lineOffsets);
|
||||
free(ta->lineVisLens);
|
||||
free(ta->rawSyntax);
|
||||
free(ta->expandBuf);
|
||||
free(ta->syntaxBuf);
|
||||
free(ta);
|
||||
w->data = NULL;
|
||||
}
|
||||
|
|
@ -1065,7 +1267,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
if (key == 1) {
|
||||
*pSA = 0;
|
||||
*pSC = *pLen;
|
||||
textAreaOffToRowCol(buf, *pLen, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, *pLen, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
textAreaEnsureVisible(w, visRows, visCols);
|
||||
wgtInvalidatePaint(w);
|
||||
|
|
@ -1099,7 +1301,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -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);
|
||||
memcpy(buf + off, clip, paste);
|
||||
*pLen += paste;
|
||||
textAreaOffToRowCol(buf, off + paste, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, off + paste, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
}
|
||||
|
||||
|
|
@ -1138,7 +1340,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
ta->desiredCol = *pCol;
|
||||
|
|
@ -1182,7 +1384,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
// Restore cursor
|
||||
int32_t restoreOff = ta->undoCursor < *pLen ? ta->undoCursor : *pLen;
|
||||
ta->undoCursor = tmpCursor;
|
||||
textAreaOffToRowCol(buf, restoreOff, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, restoreOff, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
|
|
@ -1209,7 +1411,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
}
|
||||
|
|
@ -1259,7 +1461,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
ta->desiredCol = *pCol;
|
||||
|
|
@ -1272,12 +1474,18 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t off = CUR_OFF();
|
||||
|
||||
if (off > 0) {
|
||||
char deleted = buf[off - 1];
|
||||
textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
|
||||
memmove(buf + off - 1, buf + off, *pLen - off + 1);
|
||||
(*pLen)--;
|
||||
textAreaOffToRowCol(buf, off - 1, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, off - 1, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
|
||||
if (deleted == '\n') {
|
||||
textAreaDirtyCache(w);
|
||||
} else {
|
||||
textAreaCacheDelete(w, off - 1, 1);
|
||||
}
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
|
|
@ -1298,7 +1506,7 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
ta->desiredCol = *pCol;
|
||||
|
|
@ -1311,10 +1519,16 @@ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
int32_t off = CUR_OFF();
|
||||
|
||||
if (off < *pLen) {
|
||||
char deleted = buf[off];
|
||||
textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize);
|
||||
memmove(buf + off, buf + off + 1, *pLen - off);
|
||||
(*pLen)--;
|
||||
|
||||
if (deleted == '\n') {
|
||||
textAreaDirtyCache(w);
|
||||
} else {
|
||||
textAreaCacheDelete(w, off, 1);
|
||||
}
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
|
|
@ -1334,7 +1548,7 @@ navigation:
|
|||
int32_t off = CUR_OFF();
|
||||
|
||||
if (off > 0) {
|
||||
textAreaOffToRowCol(buf, off - 1, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, off - 1, pRow, pCol);
|
||||
}
|
||||
|
||||
ta->desiredCol = *pCol;
|
||||
|
|
@ -1350,7 +1564,7 @@ navigation:
|
|||
int32_t off = CUR_OFF();
|
||||
|
||||
if (off < *pLen) {
|
||||
textAreaOffToRowCol(buf, off + 1, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, off + 1, pRow, pCol);
|
||||
}
|
||||
|
||||
ta->desiredCol = *pCol;
|
||||
|
|
@ -1365,7 +1579,7 @@ navigation:
|
|||
SEL_BEGIN();
|
||||
int32_t off = CUR_OFF();
|
||||
int32_t newOff = wordBoundaryLeft(buf, off);
|
||||
textAreaOffToRowCol(buf, newOff, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, newOff, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
SEL_END();
|
||||
textAreaEnsureVisible(w, visRows, visCols);
|
||||
|
|
@ -1378,7 +1592,7 @@ navigation:
|
|||
SEL_BEGIN();
|
||||
int32_t off = CUR_OFF();
|
||||
int32_t newOff = wordBoundaryRight(buf, *pLen, off);
|
||||
textAreaOffToRowCol(buf, newOff, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, newOff, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
SEL_END();
|
||||
textAreaEnsureVisible(w, visRows, visCols);
|
||||
|
|
@ -1489,7 +1703,7 @@ navigation:
|
|||
// Ctrl+End (scancode 0x75)
|
||||
if (key == (0x75 | 0x100)) {
|
||||
SEL_BEGIN();
|
||||
textAreaOffToRowCol(buf, *pLen, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, *pLen, pRow, pCol);
|
||||
ta->desiredCol = *pCol;
|
||||
SEL_END();
|
||||
textAreaEnsureVisible(w, visRows, visCols);
|
||||
|
|
@ -1507,7 +1721,7 @@ navigation:
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
}
|
||||
|
|
@ -1557,7 +1771,7 @@ navigation:
|
|||
int32_t hi = SEL_HI();
|
||||
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
||||
*pLen -= (hi - lo);
|
||||
textAreaOffToRowCol(buf, lo, pRow, pCol);
|
||||
textAreaOffToRowColFast(ta, lo, pRow, pCol);
|
||||
*pSA = -1;
|
||||
*pSC = -1;
|
||||
}
|
||||
|
|
@ -1570,9 +1784,10 @@ navigation:
|
|||
(*pLen)++;
|
||||
(*pCol)++;
|
||||
ta->desiredCol = *pCol;
|
||||
}
|
||||
|
||||
textAreaCacheInsert(w, off, 1);
|
||||
} else {
|
||||
textAreaDirtyCache(w);
|
||||
}
|
||||
|
||||
if (w->onChange) {
|
||||
w->onChange(w);
|
||||
|
|
@ -1803,7 +2018,7 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|||
|
||||
int32_t weRow;
|
||||
int32_t weCol;
|
||||
textAreaOffToRowCol(ta->buf, we, &weRow, &weCol);
|
||||
textAreaOffToRowColFast(ta, we, &weRow, &weCol);
|
||||
|
||||
ta->cursorRow = weRow;
|
||||
ta->cursorCol = weCol;
|
||||
|
|
@ -1959,11 +2174,8 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
break;
|
||||
}
|
||||
|
||||
// Compute line length by scanning from lineOff (not from buffer start)
|
||||
int32_t lineL = 0;
|
||||
while (lineOff + lineL < len && buf[lineOff + lineL] != '\n') {
|
||||
lineL++;
|
||||
}
|
||||
// Use cached line length (O(1) lookup instead of O(lineLen) scan)
|
||||
int32_t lineL = textAreaLineLenCached(w, row);
|
||||
int32_t drawY = textY + i * font->charHeight;
|
||||
|
||||
// Line decorator: background highlight and gutter indicators
|
||||
|
|
@ -2015,7 +2227,9 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3;
|
||||
|
||||
// Compute syntax colors for the raw line first (before expansion)
|
||||
uint8_t rawSyntax[MAX_COLORIZE_LEN];
|
||||
uint8_t *rawSyntax = ta->rawSyntax;
|
||||
char *expandBuf = ta->expandBuf;
|
||||
uint8_t *syntaxBuf = ta->syntaxBuf;
|
||||
bool hasSyntax = false;
|
||||
|
||||
if (ta->colorize && lineL > 0) {
|
||||
|
|
@ -2024,10 +2238,6 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
ta->colorize(buf + lineOff, colorLen, rawSyntax, ta->colorizeCtx);
|
||||
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 vc = 0;
|
||||
|
||||
|
|
@ -2768,6 +2978,12 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
|||
ta->captureTabs = false; // default: Tab moves focus
|
||||
ta->useTabChar = true; // default: insert actual tab character
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -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) {
|
||||
if (!w || w->type != sTextAreaTypeId) {
|
||||
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)
|
||||
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;
|
||||
|
||||
// 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
|
||||
int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol);
|
||||
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;
|
||||
|
|
@ -3147,6 +3394,7 @@ static const struct {
|
|||
void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx);
|
||||
int32_t (*getCursorLine)(const WidgetT *w);
|
||||
void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t));
|
||||
int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize);
|
||||
} sApi = {
|
||||
.create = wgtTextInput,
|
||||
.password = wgtPasswordInput,
|
||||
|
|
@ -3163,7 +3411,8 @@ static const struct {
|
|||
.replaceAll = wgtTextAreaReplaceAll,
|
||||
.setLineDecorator = wgtTextAreaSetLineDecorator,
|
||||
.getCursorLine = wgtTextAreaGetCursorLine,
|
||||
.setGutterClick = wgtTextAreaSetGutterClickCallback
|
||||
.setGutterClick = wgtTextAreaSetGutterClickCallback,
|
||||
.getWordAtCursor = wgtTextAreaGetWordAtCursor
|
||||
};
|
||||
|
||||
// Per-type APIs for the designer
|
||||
|
|
|
|||
|
|
@ -726,7 +726,8 @@ const char *widgetTreeItemGetText(const WidgetT *w) {
|
|||
|
||||
void widgetTreeItemSetText(WidgetT *w, const char *text) {
|
||||
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
||||
ti->text = text;
|
||||
free((void *)ti->text);
|
||||
ti->text = text ? strdup(text) : NULL;
|
||||
invalidateTreeDims(w);
|
||||
}
|
||||
|
||||
|
|
@ -1413,6 +1414,8 @@ static void widgetTreeViewDestroy(WidgetT *w) {
|
|||
// ============================================================
|
||||
|
||||
static void widgetTreeItemDestroy(WidgetT *w) {
|
||||
TreeItemDataT *ti = (TreeItemDataT *)w->data;
|
||||
free((void *)ti->text);
|
||||
free(w->data);
|
||||
w->data = NULL;
|
||||
}
|
||||
|
|
@ -1655,7 +1658,7 @@ WidgetT *wgtTreeItem(WidgetT *parent, const char *text) {
|
|||
|
||||
if (ti) {
|
||||
w->data = ti;
|
||||
ti->text = text;
|
||||
ti->text = text ? strdup(text) : NULL;
|
||||
ti->expanded = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue