Adding project support and breaking a lot of stuff.

This commit is contained in:
Scott Duensing 2026-03-30 22:19:38 -05:00
parent 5dd632a862
commit 82939c3f27
33 changed files with 2699 additions and 391 deletions

View file

@ -84,9 +84,10 @@ typedef struct {
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sWin = NULL;
static PrefsHandleT *sPrefs = NULL;
// Saved state for Cancel
static uint8_t sSavedColorRgb[ColorCountE][3];
@ -338,7 +339,7 @@ static void buildMouseTab(WidgetT *page) {
sDblClickSldr->weight = 100;
sDblClickSldr->onChange = onDblClickSlider;
int32_t dblMs = prefsGetInt("mouse", "doubleclick", 500);
int32_t dblMs = prefsGetInt(sPrefs, "mouse", "doubleclick", 500);
wgtSliderSetValue(sDblClickSldr, dblMs);
wgtLabel(dblRow, "Slow ");
sDblClickLbl = wgtLabel(dblRow, "");
@ -358,7 +359,7 @@ static void buildMouseTab(WidgetT *page) {
sAccelDrop->onChange = onAccelChange;
wgtDropdownSetItems(sAccelDrop, accelItems, 4);
const char *accelStr = prefsGetString("mouse", "acceleration", "medium");
const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium");
if (strcmp(accelStr, "off") == 0) {
wgtDropdownSetSelected(sAccelDrop, 0);
@ -804,9 +805,9 @@ static void onOk(WidgetT *w) {
// Save mouse settings
int32_t wheelSel = wgtDropdownGetSelected(sWheelDrop);
prefsSetString("mouse", "wheel", wheelSel == 1 ? "reversed" : "normal");
prefsSetInt("mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
prefsSetString("mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
prefsSetString(sPrefs, "mouse", "wheel", wheelSel == 1 ? "reversed" : "normal");
prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
// Save colors to INI
for (int32_t i = 0; i < ColorCountE; i++) {
@ -817,14 +818,14 @@ static void onOk(WidgetT *w) {
char val[16];
snprintf(val, sizeof(val), "%d,%d,%d", r, g, b);
prefsSetString("colors", dvxColorName((ColorIdE)i), val);
prefsSetString(sPrefs, "colors", dvxColorName((ColorIdE)i), val);
}
// Save desktop settings
if (sWallpaperPath[0]) {
prefsSetString("desktop", "wallpaper", sWallpaperPath);
prefsSetString(sPrefs, "desktop", "wallpaper", sWallpaperPath);
} else {
prefsRemove("desktop", "wallpaper");
prefsRemove(sPrefs, "desktop", "wallpaper");
}
const char *modeStr = "stretch";
@ -835,14 +836,16 @@ static void onOk(WidgetT *w) {
modeStr = "center";
}
prefsSetString("desktop", "mode", modeStr);
prefsSetString(sPrefs, "desktop", "mode", modeStr);
// Save video settings
prefsSetInt("video", "width", sAc->display.width);
prefsSetInt("video", "height", sAc->display.height);
prefsSetInt("video", "bpp", sAc->display.format.bitsPerPixel);
prefsSetInt(sPrefs, "video", "width", sAc->display.width);
prefsSetInt(sPrefs, "video", "height", sAc->display.height);
prefsSetInt(sPrefs, "video", "bpp", sAc->display.format.bitsPerPixel);
prefsSave();
prefsSave(sPrefs);
prefsClose(sPrefs);
sPrefs = NULL;
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
}
@ -851,6 +854,8 @@ static void onOk(WidgetT *w) {
static void onCancel(WidgetT *w) {
(void)w;
restoreSnapshot();
prefsClose(sPrefs);
sPrefs = NULL;
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
}
@ -858,6 +863,8 @@ static void onCancel(WidgetT *w) {
static void onClose(WindowT *win) {
restoreSnapshot();
prefsClose(sPrefs);
sPrefs = NULL;
dvxDestroyWindow(sAc, win);
sWin = NULL;
}
@ -1083,8 +1090,9 @@ static void updateSwatch(void) {
// ============================================================
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
sCtx = ctx;
sAc = ctx->shellCtx;
sPrefs = prefsLoad("CONFIG/DVX.INI");
int32_t winX = (sAc->display.width - CP_WIN_W) / 2;
int32_t winY = (sAc->display.height - CP_WIN_H) / 2;

View file

@ -31,13 +31,13 @@ COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/s
FORMRT_OBJS = $(OBJDIR)/formrt.o
# IDE app objects
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o
APP_OBJS = $(IDE_OBJS) $(FORMRT_OBJS)
APP_TARGET = $(APPDIR)/dvxbasic.app
# Native test programs (host gcc, not cross-compiled)
HOSTCC = gcc
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I.
HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. -I../../core
BINDIR = ../bin
TEST_COMPILER = $(BINDIR)/test_compiler
@ -45,10 +45,11 @@ TEST_VM = $(BINDIR)/test_vm
TEST_LEX = $(BINDIR)/test_lex
TEST_QUICK = $(BINDIR)/test_quick
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c
STB_DS_IMPL = ../../core/thirdparty/stb_ds_impl.c
TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL)
TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c $(STB_DS_IMPL)
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL)
.PHONY: all clean tests
@ -96,7 +97,10 @@ $(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/codegen.h runtime/v
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h | $(OBJDIR)

View file

@ -3,6 +3,7 @@
#include "codegen.h"
#include "symtab.h"
#include "opcodes.h"
#include "thirdparty/stb_ds_wrap.h"
#include <stdlib.h>
#include <string.h>
@ -13,11 +14,9 @@
// ============================================================
bool basAddData(BasCodeGenT *cg, BasValueT val) {
if (cg->dataCount >= BAS_MAX_CONSTANTS) {
return false;
}
cg->dataPool[cg->dataCount++] = basValCopy(val);
BasValueT copy = basValCopy(val);
arrput(cg->dataPool, copy);
cg->dataCount = (int32_t)arrlen(cg->dataPool);
return true;
}
@ -34,12 +33,10 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) {
}
}
if (cg->constCount >= BAS_MAX_CONSTANTS) {
return 0;
}
uint16_t idx = (uint16_t)cg->constCount;
cg->constants[cg->constCount++] = basStringNew(text, len);
BasStringT *s = basStringNew(text, len);
arrput(cg->constants, s);
cg->constCount = (int32_t)arrlen(cg->constants);
return idx;
}
@ -175,6 +172,12 @@ void basCodeGenFree(BasCodeGenT *cg) {
basValRelease(&cg->dataPool[i]);
}
arrfree(cg->code);
arrfree(cg->constants);
arrfree(cg->dataPool);
cg->code = NULL;
cg->constants = NULL;
cg->dataPool = NULL;
cg->constCount = 0;
cg->dataCount = 0;
cg->codeLen = 0;
@ -204,9 +207,8 @@ int32_t basCodePos(const BasCodeGenT *cg) {
// ============================================================
void basEmit8(BasCodeGenT *cg, uint8_t b) {
if (cg->codeLen < BAS_MAX_CODE) {
cg->code[cg->codeLen++] = b;
}
arrput(cg->code, b);
cg->codeLen = (int32_t)arrlen(cg->code);
}
@ -215,10 +217,11 @@ void basEmit8(BasCodeGenT *cg, uint8_t b) {
// ============================================================
void basEmit16(BasCodeGenT *cg, int16_t v) {
if (cg->codeLen + 2 <= BAS_MAX_CODE) {
memcpy(&cg->code[cg->codeLen], &v, 2);
cg->codeLen += 2;
}
uint8_t buf[2];
memcpy(buf, &v, 2);
arrput(cg->code, buf[0]);
arrput(cg->code, buf[1]);
cg->codeLen = (int32_t)arrlen(cg->code);
}
@ -227,10 +230,14 @@ void basEmit16(BasCodeGenT *cg, int16_t v) {
// ============================================================
void basEmitDouble(BasCodeGenT *cg, double v) {
if (cg->codeLen + (int32_t)sizeof(double) <= BAS_MAX_CODE) {
memcpy(&cg->code[cg->codeLen], &v, sizeof(double));
cg->codeLen += (int32_t)sizeof(double);
uint8_t buf[sizeof(double)];
memcpy(buf, &v, sizeof(double));
for (int32_t i = 0; i < (int32_t)sizeof(double); i++) {
arrput(cg->code, buf[i]);
}
cg->codeLen = (int32_t)arrlen(cg->code);
}
@ -239,10 +246,14 @@ void basEmitDouble(BasCodeGenT *cg, double v) {
// ============================================================
void basEmitFloat(BasCodeGenT *cg, float v) {
if (cg->codeLen + (int32_t)sizeof(float) <= BAS_MAX_CODE) {
memcpy(&cg->code[cg->codeLen], &v, sizeof(float));
cg->codeLen += (int32_t)sizeof(float);
uint8_t buf[sizeof(float)];
memcpy(buf, &v, sizeof(float));
for (int32_t i = 0; i < (int32_t)sizeof(float); i++) {
arrput(cg->code, buf[i]);
}
cg->codeLen = (int32_t)arrlen(cg->code);
}
@ -251,10 +262,11 @@ void basEmitFloat(BasCodeGenT *cg, float v) {
// ============================================================
void basEmitU16(BasCodeGenT *cg, uint16_t v) {
if (cg->codeLen + 2 <= BAS_MAX_CODE) {
memcpy(&cg->code[cg->codeLen], &v, 2);
cg->codeLen += 2;
}
uint8_t buf[2];
memcpy(buf, &v, 2);
arrput(cg->code, buf[0]);
arrput(cg->code, buf[1]);
cg->codeLen = (int32_t)arrlen(cg->code);
}

View file

@ -19,16 +19,13 @@
// Code generator state
// ============================================================
#define BAS_MAX_CODE 65536
#define BAS_MAX_CONSTANTS 1024
typedef struct {
uint8_t code[BAS_MAX_CODE];
uint8_t *code; // stb_ds dynamic array
int32_t codeLen;
BasStringT *constants[BAS_MAX_CONSTANTS];
BasStringT **constants; // stb_ds dynamic array
int32_t constCount;
int32_t globalCount;
BasValueT dataPool[BAS_MAX_CONSTANTS];
BasValueT *dataPool; // stb_ds dynamic array
int32_t dataCount;
} BasCodeGenT;

View file

@ -7,6 +7,7 @@
#include "parser.h"
#include "opcodes.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <stdio.h>
@ -529,8 +530,8 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) {
basEmit8(&p->cg, baseSlot);
// If not yet defined, record the address for backpatching
if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) {
sym->patchAddrs[sym->patchCount++] = addrPos;
if (!sym->isDefined && true) {
arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
@ -574,9 +575,8 @@ static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName
basEmit16(&p->cg, 0);
// Record patch address for backpatching when label is defined
if (sym->patchCount < BAS_MAX_CALL_PATCHES) {
sym->patchAddrs[sym->patchCount++] = patchAddr;
}
arrput(sym->patchAddrs, patchAddr);
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
@ -1616,8 +1616,8 @@ static void parseAssignOrCall(BasParserT *p) {
basEmit8(&p->cg, (uint8_t)argc);
basEmit8(&p->cg, baseSlot);
if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) {
sym->patchAddrs[sym->patchCount++] = addrPos;
if (!sym->isDefined && true) {
arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
@ -2248,12 +2248,17 @@ static void parseDimBounds(BasParserT *p, int32_t *outDims) {
int32_t exprLen = basCodePos(&p->cg) - exprStart;
int32_t insertLen = 3; // OP_PUSH_INT16 + 2 bytes
if (basCodePos(&p->cg) + insertLen <= BAS_MAX_CODE) {
{
// Grow the array to make room for the insertion
for (int32_t pad = 0; pad < insertLen; pad++) {
arrput(p->cg.code, 0);
}
memmove(&p->cg.code[exprStart + insertLen], &p->cg.code[exprStart], exprLen);
p->cg.code[exprStart] = OP_PUSH_INT16;
int16_t lbound = (int16_t)p->optionBase;
memcpy(&p->cg.code[exprStart + 1], &lbound, 2);
p->cg.codeLen += insertLen;
p->cg.codeLen = (int32_t)arrlen(p->cg.code);
}
}
@ -3289,9 +3294,8 @@ static void parseOnError(BasParserT *p) {
int32_t patchAddr = basCodePos(&p->cg);
basEmit16(&p->cg, 0);
if (sym->patchCount < BAS_MAX_CALL_PATCHES) {
sym->patchAddrs[sym->patchCount++] = patchAddr;
}
arrput(sym->patchAddrs, patchAddr);
sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
@ -4178,8 +4182,8 @@ static void parseStatement(BasParserT *p) {
basEmit8(&p->cg, 0);
basEmit8(&p->cg, baseSlot);
if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) {
sym->patchAddrs[sym->patchCount++] = addrPos;
if (!sym->isDefined && true) {
arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs);
}
}
@ -4513,26 +4517,23 @@ static void parseType(BasParserT *p) {
return;
}
if (typeSym->fieldCount >= BAS_MAX_UDT_FIELDS) {
error(p, "Too many fields in TYPE");
return;
}
BasFieldDefT *field = &typeSym->fields[typeSym->fieldCount];
BasFieldDefT field;
memset(&field, 0, sizeof(field));
// Truncation is intentional -- field names are clamped to BAS_MAX_SYMBOL_NAME.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
snprintf(field->name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text);
snprintf(field.name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text);
#pragma GCC diagnostic pop
advance(p);
expect(p, TOK_AS);
field->dataType = resolveTypeName(p);
if (field->dataType == BAS_TYPE_UDT) {
field->udtTypeId = p->lastUdtTypeId;
field.dataType = resolveTypeName(p);
if (field.dataType == BAS_TYPE_UDT) {
field.udtTypeId = p->lastUdtTypeId;
}
typeSym->fieldCount++;
arrput(typeSym->fields, field);
typeSym->fieldCount = (int32_t)arrlen(typeSym->fields);
expectEndOfStatement(p);
skipNewlines(p);
@ -4806,4 +4807,14 @@ BasModuleT *basParserBuildModule(BasParserT *p) {
void basParserFree(BasParserT *p) {
basCodeGenFree(&p->cg);
// Free per-symbol dynamic arrays
for (int32_t i = 0; i < p->sym.count; i++) {
arrfree(p->sym.symbols[i].patchAddrs);
arrfree(p->sym.symbols[i].fields);
}
arrfree(p->sym.symbols);
p->sym.symbols = NULL;
p->sym.count = 0;
}

View file

@ -1,8 +1,10 @@
// symtab.c -- DVX BASIC symbol table implementation
#include "symtab.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
@ -31,10 +33,6 @@ static bool namesEqual(const char *a, const char *b) {
// ============================================================
BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) {
if (tab->count >= BAS_MAX_SYMBOLS) {
return NULL;
}
// Check for duplicate in current scope
BasScopeE scope = tab->inLocalScope ? SCOPE_LOCAL : SCOPE_GLOBAL;
@ -44,8 +42,11 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
}
}
BasSymbolT *sym = &tab->symbols[tab->count++];
memset(sym, 0, sizeof(*sym));
BasSymbolT entry;
memset(&entry, 0, sizeof(entry));
arrput(tab->symbols, entry);
tab->count = (int32_t)arrlen(tab->symbols);
BasSymbolT *sym = &tab->symbols[tab->count - 1];
strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1);
sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0';
sym->kind = kind;
@ -128,11 +129,14 @@ void basSymTabInit(BasSymTabT *tab) {
// ============================================================
void basSymTabLeaveLocal(BasSymTabT *tab) {
// Remove all local symbols
// Remove all local symbols, freeing their dynamic arrays
int32_t newCount = 0;
for (int32_t i = 0; i < tab->count; i++) {
if (tab->symbols[i].scope != SCOPE_LOCAL) {
if (tab->symbols[i].scope == SCOPE_LOCAL) {
arrfree(tab->symbols[i].patchAddrs);
arrfree(tab->symbols[i].fields);
} else {
if (i != newCount) {
tab->symbols[newCount] = tab->symbols[i];
}
@ -141,6 +145,7 @@ void basSymTabLeaveLocal(BasSymTabT *tab) {
}
}
arrsetlen(tab->symbols, newCount);
tab->count = newCount;
tab->inLocalScope = false;
tab->nextLocalIdx = 0;

View file

@ -42,8 +42,6 @@ typedef enum {
#define BAS_MAX_SYMBOL_NAME 64
#define BAS_MAX_PARAMS 16
#define BAS_MAX_CALL_PATCHES 32
#define BAS_MAX_UDT_FIELDS 32
// UDT field definition
typedef struct {
@ -74,7 +72,7 @@ typedef struct {
bool paramByVal[BAS_MAX_PARAMS];
// Forward-reference backpatch list (code addresses to patch when defined)
int32_t patchAddrs[BAS_MAX_CALL_PATCHES];
int32_t *patchAddrs; // stb_ds dynamic array
int32_t patchCount;
// For CONST: the constant value
@ -85,19 +83,17 @@ typedef struct {
char constStr[256];
// For TYPE_DEF: field definitions
BasFieldDefT fields[BAS_MAX_UDT_FIELDS];
int32_t fieldCount;
BasFieldDefT *fields; // stb_ds dynamic array
int32_t fieldCount;
} BasSymbolT;
// ============================================================
// Symbol table
// ============================================================
#define BAS_MAX_SYMBOLS 512
typedef struct {
BasSymbolT symbols[BAS_MAX_SYMBOLS];
int32_t count;
BasSymbolT *symbols; // stb_ds dynamic array
int32_t count;
int32_t nextGlobalIdx; // next global variable slot
int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION)
bool inLocalScope; // true when inside SUB/FUNCTION

View file

@ -10,3 +10,5 @@ tb_run icon tb_run.bmp
tb_stop icon tb_stop.bmp
tb_code icon tb_code.bmp
tb_design icon tb_design.bmp
# Placeholder icon (32x32)
noicon icon noicon.bmp

View file

@ -10,6 +10,7 @@
#include "dvxDialog.h"
#include "dvxWm.h"
#include "widgetBox.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <stdio.h>
@ -230,7 +231,7 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const
(void)ctx;
BasFormT *form = (BasFormT *)formRef;
if (!form || form->controlCount >= BAS_MAX_CTRLS) {
if (!form) {
return NULL;
}
@ -257,8 +258,12 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const
wgtSetName(widget, ctrlName);
// Initialize control entry
BasControlT *ctrl = &form->controls[form->controlCount++];
memset(ctrl, 0, sizeof(*ctrl));
BasControlT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
arrput(form->controls, entry);
form->controlCount = (int32_t)arrlen(form->controls);
BasControlT *ctrl = &form->controls[form->controlCount - 1];
snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
ctrl->widget = widget;
ctrl->form = form;
@ -296,12 +301,15 @@ void basFormRtDestroy(BasFormRtT *rt) {
freeListBoxItems(&form->controls[j]);
}
arrfree(form->controls);
if (form->window) {
dvxDestroyWindow(rt->ctx, form->window);
form->window = NULL;
}
}
arrfree(rt->forms);
free(rt);
}
@ -454,10 +462,6 @@ static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *
void *basFormRtLoadForm(void *ctx, const char *formName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
if (rt->formCount >= BAS_MAX_FORMS) {
return NULL;
}
// Check if form already exists
for (int32_t i = 0; i < rt->formCount; i++) {
if (strcasecmp(rt->forms[i].name, formName) == 0) {
@ -478,8 +482,11 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
return NULL;
}
BasFormT *form = &rt->forms[rt->formCount++];
memset(form, 0, sizeof(*form));
BasFormT entry;
memset(&entry, 0, sizeof(entry));
arrput(rt->forms, entry);
rt->formCount = (int32_t)arrlen(rt->forms);
BasFormT *form = &rt->forms[rt->formCount - 1];
snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName);
win->onClose = onFormClose;
win->onResize = onFormResize;
@ -626,15 +633,19 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
wgtSetName(widget, ctrlName);
if (form->controlCount < BAS_MAX_CTRLS) {
current = &form->controls[form->controlCount++];
memset(current, 0, sizeof(*current));
snprintf(current->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
snprintf(current->typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
current->widget = widget;
current->form = form;
current->iface = wgtGetIface(wgtTypeName);
{
BasControlT ctrlEntry;
memset(&ctrlEntry, 0, sizeof(ctrlEntry));
snprintf(ctrlEntry.name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
snprintf(ctrlEntry.typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
ctrlEntry.widget = widget;
ctrlEntry.form = form;
ctrlEntry.iface = wgtGetIface(wgtTypeName);
arrput(form->controls, ctrlEntry);
form->controlCount = (int32_t)arrlen(form->controls);
// Re-derive pointer after arrput (may realloc)
current = &form->controls[form->controlCount - 1];
widget->userData = current;
widget->onClick = onWidgetClick;
widget->onDblClick = onWidgetDblClick;
@ -779,6 +790,14 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen
form->window->x = form->frmLeft;
form->window->y = form->frmTop;
}
// Re-wire widget->userData pointers now that the controls array
// is finalized. arrput may have reallocated during loading, so
// any userData set during parsing could be stale.
for (int32_t i = 0; i < form->controlCount; i++) {
if (form->controls[i].widget) {
form->controls[i].widget->userData = &form->controls[i];
}
}
}
return form;

View file

@ -27,8 +27,6 @@ typedef struct BasControlT BasControlT;
// ============================================================
#define BAS_MAX_CTRL_NAME 32
#define BAS_MAX_CTRLS 64 // max controls per form
#define BAS_MAX_FORMS 8
// ============================================================
// Control instance (a widget on a form)
@ -55,7 +53,7 @@ typedef struct BasFormT {
WidgetT *root; // widget root (from wgtInitWindow)
WidgetT *contentBox; // VBox/HBox for user controls
AppContextT *ctx; // DVX app context
BasControlT controls[BAS_MAX_CTRLS]; // controls on this form
BasControlT *controls; // stb_ds dynamic array
int32_t controlCount;
BasVmT *vm; // VM for event dispatch
BasModuleT *module; // compiled module (for SUB lookup)
@ -79,7 +77,7 @@ typedef struct {
AppContextT *ctx; // DVX app context
BasVmT *vm; // shared VM instance
BasModuleT *module; // compiled module
BasFormT forms[BAS_MAX_FORMS];
BasFormT *forms; // stb_ds dynamic array
int32_t formCount;
BasFormT *currentForm; // form currently dispatching events
} BasFormRtT;

View file

@ -524,7 +524,7 @@ void dsgnNewForm(DsgnStateT *ds, const char *name) {
form->top = 0;
snprintf(form->layout, DSGN_MAX_NAME, "VBox");
form->centered = true;
form->autoSize = true;
form->autoSize = false;
form->resizable = true;
snprintf(form->name, DSGN_MAX_NAME, "%s", name);
snprintf(form->caption, DSGN_MAX_TEXT, "%s", name);

View file

@ -175,4 +175,14 @@ bool dsgnIsContainer(const char *typeName);
// Free designer resources.
void dsgnFree(DsgnStateT *ds);
// ============================================================
// Code rename support (implemented in ideMain.c)
// ============================================================
//
// Rename all references to a form or control in project .bas files.
// Replaces OldName. -> NewName. and OldName_ -> NewName_ (case-insensitive,
// word-boundary aware). Also handles FormName.ControlName. patterns.
void ideRenameInCode(const char *oldName, const char *newName);
#endif // IDE_DESIGNER_H

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,853 @@
// ideProject.c -- DVX BASIC project file management and project window
//
// The .dbp (DVX BASIC Project) file is INI-format:
//
// [Project]
// Name = MyProject
//
// [Modules]
// Count = 2
// File0 = MAIN.BAS
// File1 = UTILS.BAS
//
// [Forms]
// Count = 1
// File0 = FORM1.FRM
//
// [Settings]
// StartupForm = Form1
//
// All file paths are relative to the directory containing the .dbp file.
// Uses the handle-based dvxPrefs API with a dedicated handle per load/save
// so project files don't interfere with the IDE's own preferences.
#include "ideProject.h"
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxPrefs.h"
#include "dvxWm.h"
#include "widgetBox.h"
#include "widgetButton.h"
#include "widgetImage.h"
#include "widgetLabel.h"
#include "widgetTextInput.h"
#include "widgetTreeView.h"
#include "thirdparty/stb_ds_wrap.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Constants
// ============================================================
#define PRJ_WIN_W 180
#define PRJ_WIN_H 300
#define PRJ_MAX_FILES 256
// ============================================================
// Module state
// ============================================================
static PrjStateT *sPrj = NULL;
static WindowT *sPrjWin = NULL;
static WidgetT *sTree = NULL;
static PrjFileClickFnT sOnClick = NULL;
static char **sLabels = NULL; // stb_ds array of strdup'd strings
// ============================================================
// Prototypes
// ============================================================
static void onPrjWinClose(WindowT *win);
static void onTreeItemClick(WidgetT *w);
// ============================================================
// prjInit
// ============================================================
void prjInit(PrjStateT *prj) {
memset(prj, 0, sizeof(*prj));
prj->activeFileIdx = -1;
}
// ============================================================
// prjClose
// ============================================================
void prjClose(PrjStateT *prj) {
for (int32_t i = 0; i < prj->fileCount; i++) {
free(prj->files[i].buffer);
}
arrfree(prj->files);
arrfree(prj->sourceMap);
prjInit(prj);
}
// ============================================================
// prjLoad
// ============================================================
bool prjLoad(PrjStateT *prj, const char *dbpPath) {
PrefsHandleT *h = prefsLoad(dbpPath);
if (!h) {
return false;
}
prjInit(prj);
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Derive project directory
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = strrchr(prj->projectDir, '/');
char *sep2 = strrchr(prj->projectDir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
// [Project] section
const char *val;
val = prefsGetString(h, "Project", "Name", NULL);
if (val) { snprintf(prj->name, sizeof(prj->name), "%s", val); }
val = prefsGetString(h, "Project", "Author", NULL);
if (val) { snprintf(prj->author, sizeof(prj->author), "%s", val); }
val = prefsGetString(h, "Project", "Company", NULL);
if (val) { snprintf(prj->company, sizeof(prj->company), "%s", val); }
val = prefsGetString(h, "Project", "Version", NULL);
if (val) { snprintf(prj->version, sizeof(prj->version), "%s", val); }
val = prefsGetString(h, "Project", "Copyright", NULL);
if (val) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", val); }
val = prefsGetString(h, "Project", "Description", NULL);
if (val) { snprintf(prj->description, sizeof(prj->description), "%s", val); }
val = prefsGetString(h, "Project", "Icon", NULL);
if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); }
// [Modules] section -- File0, File1, ...
for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
val = prefsGetString(h, "Modules", key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, false);
}
// [Forms] section -- File0, File1, ...
for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)i);
val = prefsGetString(h, "Forms", key, NULL);
if (!val) {
break;
}
prjAddFile(prj, val, true);
}
// [Settings] section
val = prefsGetString(h, "Settings", "StartupForm", NULL);
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }
prefsClose(h);
prj->dirty = false;
return true;
}
// ============================================================
// prjSave
// ============================================================
bool prjSave(const PrjStateT *prj) {
if (prj->projectPath[0] == '\0') {
return false;
}
PrefsHandleT *h = prefsCreate();
if (!h) {
return false;
}
// [Project] section
prefsSetString(h, "Project", "Name", prj->name);
if (prj->author[0]) { prefsSetString(h, "Project", "Author", prj->author); }
if (prj->company[0]) { prefsSetString(h, "Project", "Company", prj->company); }
if (prj->version[0]) { prefsSetString(h, "Project", "Version", prj->version); }
if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); }
if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); }
if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); }
// [Modules] section
int32_t modIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)modIdx++);
prefsSetString(h, "Modules", key, prj->files[i].path);
}
}
prefsSetInt(h, "Modules", "Count", modIdx);
// [Forms] section
int32_t frmIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm) {
char key[16];
snprintf(key, sizeof(key), "File%d", (int)frmIdx++);
prefsSetString(h, "Forms", key, prj->files[i].path);
}
}
prefsSetInt(h, "Forms", "Count", frmIdx);
// [Settings] section
prefsSetString(h, "Settings", "StartupForm", prj->startupForm);
bool ok = prefsSaveAs(h, prj->projectPath);
prefsClose(h);
return ok;
}
// ============================================================
// prjSaveAs
// ============================================================
bool prjSaveAs(PrjStateT *prj, const char *dbpPath) {
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);
// Update project directory
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
char *sep = strrchr(prj->projectDir, '/');
char *sep2 = strrchr(prj->projectDir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
prj->projectDir[0] = '.';
prj->projectDir[1] = '\0';
}
return prjSave(prj);
}
// ============================================================
// prjNew
// ============================================================
void prjNew(PrjStateT *prj, const char *name, const char *directory) {
prjInit(prj);
snprintf(prj->name, sizeof(prj->name), "%s", name);
snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory);
snprintf(prj->projectPath, sizeof(prj->projectPath), "%s/%s.dbp", directory, name);
prj->dirty = true;
}
// ============================================================
// prjAddFile
// ============================================================
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
PrjFileT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.path, sizeof(entry.path), "%s", relativePath);
entry.isForm = isForm;
arrput(prj->files, entry);
prj->fileCount = (int32_t)arrlen(prj->files);
prj->dirty = true;
return prj->fileCount - 1;
}
// ============================================================
// prjRemoveFile
// ============================================================
void prjRemoveFile(PrjStateT *prj, int32_t idx) {
if (idx < 0 || idx >= prj->fileCount) {
return;
}
free(prj->files[idx].buffer);
arrdel(prj->files, idx);
prj->fileCount = (int32_t)arrlen(prj->files);
// Adjust active file index
if (prj->activeFileIdx == idx) {
prj->activeFileIdx = -1;
} else if (prj->activeFileIdx > idx) {
prj->activeFileIdx--;
}
prj->dirty = true;
}
// ============================================================
// prjFullPath
// ============================================================
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) {
if (fileIdx < 0 || fileIdx >= prj->fileCount) {
outPath[0] = '\0';
return;
}
snprintf(outPath, outSize, "%s/%s", prj->projectDir, prj->files[fileIdx].path);
}
// ============================================================
// prjMapLine
// ============================================================
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
for (int32_t i = 0; i < prj->sourceMapCount; i++) {
const PrjSourceMapT *m = &prj->sourceMap[i];
if (concatLine >= m->startLine && concatLine < m->startLine + m->lineCount) {
*outFileIdx = m->fileIdx;
*outLocalLine = concatLine - m->startLine + 1;
return true;
}
}
return false;
}
// ============================================================
// Project window callbacks
// ============================================================
static void onPrjWinClose(WindowT *win) {
(void)win;
}
static void onTreeItemClick(WidgetT *w) {
if (!sPrj || !sOnClick) {
return;
}
int32_t fileIdx = (int32_t)(intptr_t)w->userData;
if (fileIdx >= 0 && fileIdx < sPrj->fileCount) {
sOnClick(fileIdx, sPrj->files[fileIdx].isForm);
}
}
// ============================================================
// prjCreateWindow
// ============================================================
WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick) {
sPrj = prj;
sOnClick = onClick;
sPrjWin = dvxCreateWindow(ctx, "Project", 0, 250, PRJ_WIN_W, PRJ_WIN_H, true);
if (!sPrjWin) {
return NULL;
}
sPrjWin->onClose = onPrjWinClose;
WidgetT *root = wgtInitWindow(ctx, sPrjWin);
sTree = wgtTreeView(root);
sTree->weight = 100;
prjRebuildTree(prj);
return sPrjWin;
}
// ============================================================
// prjDestroyWindow
// ============================================================
void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
if (win) {
dvxDestroyWindow(ctx, win);
}
// Free label strings
if (sLabels) {
for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) {
free(sLabels[i]);
}
arrfree(sLabels);
sLabels = NULL;
}
sPrjWin = NULL;
sTree = NULL;
sPrj = NULL;
sOnClick = NULL;
}
// ============================================================
// prjRebuildTree
// ============================================================
void prjRebuildTree(PrjStateT *prj) {
if (!sTree) {
return;
}
// Clear existing items by removing all children
sTree->firstChild = NULL;
sTree->lastChild = NULL;
// Free old labels
if (sLabels) {
for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) {
free(sLabels[i]);
}
arrfree(sLabels);
sLabels = NULL;
}
if (!prj || prj->fileCount == 0) {
return;
}
// Project name as root
char *projLabel = strdup(prj->name[0] ? prj->name : "Project");
arrput(sLabels, projLabel);
WidgetT *projNode = wgtTreeItem(sTree, projLabel);
projNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(projNode, true);
// Forms group
char *formsLabel = strdup("Forms");
arrput(sLabels, formsLabel);
WidgetT *formsNode = wgtTreeItem(projNode, formsLabel);
formsNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(formsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm) {
char *label = strdup(prj->files[i].path);
arrput(sLabels, label);
WidgetT *item = wgtTreeItem(formsNode, label);
item->userData = (void *)(intptr_t)i;
item->onClick = onTreeItemClick;
}
}
// Modules group
char *modsLabel = strdup("Modules");
arrput(sLabels, modsLabel);
WidgetT *modsNode = wgtTreeItem(projNode, modsLabel);
modsNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(modsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
char *label = strdup(prj->files[i].path);
arrput(sLabels, label);
WidgetT *item = wgtTreeItem(modsNode, label);
item->userData = (void *)(intptr_t)i;
item->onClick = onTreeItemClick;
}
}
wgtInvalidate(sTree);
}
// ============================================================
// Project properties dialog
// ============================================================
#define PPD_WIDTH 380
#define PPD_LABEL_W 96
#define PPD_BTN_W 70
#define PPD_BTN_H 24
#define PPD_DESC_H 60
static struct {
bool done;
bool accepted;
WidgetT *name;
WidgetT *author;
WidgetT *company;
WidgetT *version;
WidgetT *copyright;
WidgetT *description;
WidgetT *iconPreview;
char iconPath[DVX_MAX_PATH];
const char *appPath;
AppContextT *ctx;
PrjStateT *prj;
} sPpd;
static void ppdOnOk(WidgetT *w) {
(void)w;
// Validate icon path if set
if (sPpd.iconPath[0] && sPpd.prj) {
const char *iconText = sPpd.iconPath;
char fullPath[DVX_MAX_PATH * 2];
snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, iconText);
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
return;
}
if (infoW != 32 || infoH != 32) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
return;
}
}
sPpd.accepted = true;
sPpd.done = true;
}
static void ppdOnCancel(WidgetT *w) { (void)w; sPpd.accepted = false; sPpd.done = true; }
static void ppdOnClose(WindowT *win) { (void)win; sPpd.accepted = false; sPpd.done = true; }
static void ppdLoadIconPreview(void) {
if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) {
return;
}
if (!sPpd.iconPath[0]) {
return;
}
const char *relPath = sPpd.iconPath;
char fullPath[DVX_MAX_PATH * 2];
snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, relPath);
// Verify the image is 32x32 before loading
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
return;
}
if (infoW != 32 || infoH != 32) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
sPpd.iconPath[0] = '\0';
return;
}
int32_t w = 0;
int32_t h = 0;
int32_t pitch = 0;
uint8_t *data = dvxLoadImage(sPpd.ctx, fullPath, &w, &h, &pitch);
if (data) {
wgtImageSetData(sPpd.iconPreview, data, w, h, pitch);
}
}
static void ppdOnBrowseIcon(WidgetT *w) {
(void)w;
FileFilterT filters[] = {
{ "Images (*.bmp;*.png;*.jpg;*.gif)", "*.bmp;*.png;*.jpg;*.gif" },
{ "All Files (*.*)", "*.*" }
};
char path[DVX_MAX_PATH];
if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
// Validate size using the full path before accepting
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(path, &infoW, &infoH)) {
dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
return;
}
if (infoW != 32 || infoH != 32) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
return;
}
// The icon must be in the project directory so the relative
// path works when the project is reloaded.
const char *relPath = NULL;
int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);
if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
(path[dirLen] == '/' || path[dirLen] == '\\')) {
relPath = path + dirLen + 1;
}
if (!relPath) {
int32_t result = dvxMessageBox(sPpd.ctx, "Copy Icon",
"The icon is outside the project directory.\nCopy it to the project?",
MB_YESNO | MB_ICONQUESTION);
if (result != ID_YES) {
return;
}
// Get just the filename
const char *fname = strrchr(path, '/');
const char *fname2 = strrchr(path, '\\');
if (fname2 > fname) {
fname = fname2;
}
fname = fname ? fname + 1 : path;
// Check if destination already exists
char destPath[DVX_MAX_PATH * 2];
snprintf(destPath, sizeof(destPath), "%s/%s", sPpd.prj->projectDir, fname);
FILE *existing = fopen(destPath, "rb");
if (existing) {
fclose(existing);
char msg[DVX_MAX_PATH + 32];
snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);
if (ow != ID_YES) {
return;
}
}
// Copy the file
FILE *src = fopen(path, "rb");
if (!src) {
dvxMessageBox(sPpd.ctx, "Error", "Could not read source file.", MB_OK | MB_ICONERROR);
return;
}
FILE *dst = fopen(destPath, "wb");
if (!dst) {
fclose(src);
dvxMessageBox(sPpd.ctx, "Error", "Could not write to project directory.", MB_OK | MB_ICONERROR);
return;
}
char buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
fwrite(buf, 1, n, dst);
}
fclose(src);
fclose(dst);
relPath = fname;
}
snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", relPath);
ppdLoadIconPreview();
}
}
static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) {
WidgetT *row = wgtHBox(parent);
row->spacing = wgtPixels(4);
WidgetT *lbl = wgtLabel(row, labelText);
lbl->minW = wgtPixels(PPD_LABEL_W);
WidgetT *input = wgtTextInput(row, maxLen);
input->weight = 100;
wgtSetText(input, value);
return input;
}
bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) {
if (!ctx || !prj) {
return false;
}
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false);
if (!win) {
return false;
}
win->modal = true;
win->onClose = ppdOnClose;
win->maxW = win->w;
win->maxH = win->h;
sPpd.done = false;
sPpd.accepted = false;
sPpd.ctx = ctx;
sPpd.prj = prj;
sPpd.appPath = appPath;
WidgetT *root = wgtInitWindow(ctx, win);
if (!root) {
dvxDestroyWindow(ctx, win);
return false;
}
root->spacing = wgtPixels(2);
sPpd.name = ppdAddRow(root, "Name:", prj->name, PRJ_MAX_NAME);
sPpd.author = ppdAddRow(root, "Author:", prj->author, PRJ_MAX_STRING);
sPpd.company = ppdAddRow(root, "Company:", prj->company, PRJ_MAX_STRING);
sPpd.version = ppdAddRow(root, "Version:", prj->version, PRJ_MAX_NAME);
sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING);
// Icon row: label + preview + Browse button
{
WidgetT *iconRow = wgtHBox(root);
iconRow->spacing = wgtPixels(4);
WidgetT *iconLbl = wgtLabel(iconRow, "Icon:");
iconLbl->minW = wgtPixels(PPD_LABEL_W);
// Load "noicon" placeholder from app resources
int32_t niW = 0;
int32_t niH = 0;
int32_t niP = 0;
uint8_t *noIconData = appPath ? dvxResLoadIcon(ctx, appPath, "noicon", &niW, &niH, &niP) : NULL;
if (noIconData) {
sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP);
} else {
uint8_t *placeholder = (uint8_t *)calloc(4, 1);
sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, 4);
}
WidgetT *browseBtn = wgtButton(iconRow, "Browse...");
browseBtn->onClick = ppdOnBrowseIcon;
snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", prj->iconPath);
ppdLoadIconPreview();
}
// Description gets a taller text area
WidgetT *descRow = wgtHBox(root);
descRow->spacing = wgtPixels(4);
WidgetT *descLbl = wgtLabel(descRow, "Description:");
descLbl->minW = wgtPixels(PPD_LABEL_W);
sPpd.description = wgtTextArea(descRow, PRJ_MAX_DESC);
sPpd.description->weight = 100;
sPpd.description->minH = wgtPixels(PPD_DESC_H);
wgtSetText(sPpd.description, prj->description);
// OK / Cancel buttons
WidgetT *btnRow = wgtHBox(root);
btnRow->align = AlignCenterE;
WidgetT *okBtn = wgtButton(btnRow, "&OK");
okBtn->minW = wgtPixels(PPD_BTN_W);
okBtn->minH = wgtPixels(PPD_BTN_H);
okBtn->onClick = ppdOnOk;
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
cancelBtn->minW = wgtPixels(PPD_BTN_W);
cancelBtn->minH = wgtPixels(PPD_BTN_H);
cancelBtn->onClick = ppdOnCancel;
dvxFitWindow(ctx, win);
WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win;
while (!sPpd.done && ctx->running) {
dvxUpdate(ctx);
}
if (sPpd.accepted) {
const char *s;
s = wgtGetText(sPpd.name);
if (s) { snprintf(prj->name, sizeof(prj->name), "%s", s); }
s = wgtGetText(sPpd.author);
if (s) { snprintf(prj->author, sizeof(prj->author), "%s", s); }
s = wgtGetText(sPpd.company);
if (s) { snprintf(prj->company, sizeof(prj->company), "%s", s); }
s = wgtGetText(sPpd.version);
if (s) { snprintf(prj->version, sizeof(prj->version), "%s", s); }
s = wgtGetText(sPpd.copyright);
if (s) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", s); }
s = wgtGetText(sPpd.description);
if (s) { snprintf(prj->description, sizeof(prj->description), "%s", s); }
snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);
prj->dirty = true;
}
ctx->modalWindow = prevModal;
dvxDestroyWindow(ctx, win);
return sPpd.accepted;
}

View file

@ -0,0 +1,105 @@
// ideProject.h -- DVX BASIC project file management and project window
#ifndef IDE_PROJECT_H
#define IDE_PROJECT_H
#include "dvxApp.h"
#include "dvxTypes.h"
#include <stdbool.h>
#include <stdint.h>
// ============================================================
// Constants
// ============================================================
#define PRJ_MAX_NAME 32
#define PRJ_MAX_STRING 128
#define PRJ_MAX_DESC 512
// ============================================================
// Project file entry
// ============================================================
typedef struct {
char path[DVX_MAX_PATH]; // relative path (8.3 DOS name)
bool isForm; // true = .frm, false = .bas
char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded)
bool modified; // true = buffer has unsaved changes
} PrjFileT;
// ============================================================
// Source map entry (for multi-file error reporting)
// ============================================================
typedef struct {
int32_t startLine; // 1-based line in concatenated source
int32_t lineCount; // lines contributed by this file
int32_t fileIdx; // index in PrjStateT.files[]
} PrjSourceMapT;
// ============================================================
// Project state
// ============================================================
typedef struct {
char name[PRJ_MAX_NAME];
char projectPath[DVX_MAX_PATH]; // full path to .dbp file
char projectDir[DVX_MAX_PATH]; // directory containing .dbp
char startupForm[PRJ_MAX_NAME];
// Project metadata (for binary generation)
char author[PRJ_MAX_STRING];
char company[PRJ_MAX_STRING];
char version[PRJ_MAX_NAME];
char copyright[PRJ_MAX_STRING];
char description[PRJ_MAX_DESC];
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
PrjFileT *files; // stb_ds dynamic array
int32_t fileCount;
PrjSourceMapT *sourceMap; // stb_ds dynamic array
int32_t sourceMapCount;
bool dirty;
int32_t activeFileIdx; // index of file open in editor (-1 = none)
} PrjStateT;
// ============================================================
// Project management
// ============================================================
void prjInit(PrjStateT *prj);
bool prjLoad(PrjStateT *prj, const char *dbpPath);
bool prjSave(const PrjStateT *prj);
bool prjSaveAs(PrjStateT *prj, const char *dbpPath);
void prjNew(PrjStateT *prj, const char *name, const char *directory);
void prjClose(PrjStateT *prj);
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
void prjRemoveFile(PrjStateT *prj, int32_t idx);
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);
// ============================================================
// Source map -- translate concatenated line to file + local line
// ============================================================
// Returns true if the line was found in the map. Sets outFileIdx and
// outLocalLine to the originating file and line within that file.
bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine);
// ============================================================
// Project window UI
// ============================================================
typedef void (*PrjFileClickFnT)(int32_t fileIdx, bool isForm);
WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick);
void prjDestroyWindow(AppContextT *ctx, WindowT *win);
void prjRebuildTree(PrjStateT *prj);
// ============================================================
// Project properties dialog
// ============================================================
// Show a modal dialog for editing project metadata. Returns true if
// the user clicked OK (fields in prj are updated), false if cancelled.
bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath);
#endif // IDE_PROJECT_H

View file

@ -288,12 +288,15 @@ static void onPropDblClick(WidgetT *w) {
DsgnControlT *ctrl = &sDs->form->controls[sDs->selectedIdx];
if (strcasecmp(propName, "Name") == 0) {
char oldName[DSGN_MAX_NAME];
snprintf(oldName, sizeof(oldName), "%s", ctrl->name);
snprintf(ctrl->name, DSGN_MAX_NAME, "%.31s", newValue);
if (ctrl->widget) {
wgtSetName(ctrl->widget, ctrl->name);
}
ideRenameInCode(oldName, ctrl->name);
prpRebuildTree(sDs);
} else if (strcasecmp(propName, "MinWidth") == 0) {
ctrl->width = atoi(newValue);
@ -433,7 +436,23 @@ static void onPropDblClick(WidgetT *w) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
} else {
if (strcasecmp(propName, "Caption") == 0) {
if (strcasecmp(propName, "Name") == 0) {
char oldName[DSGN_MAX_NAME];
snprintf(oldName, sizeof(oldName), "%s", sDs->form->name);
// Length-clamped memcpy instead of strncpy/snprintf because
// GCC warns about both when source exceeds the buffer.
int32_t nl = (int32_t)strlen(newValue);
if (nl >= DSGN_MAX_NAME) {
nl = DSGN_MAX_NAME - 1;
}
memcpy(sDs->form->name, newValue, nl);
sDs->form->name[nl] = '\0';
ideRenameInCode(oldName, sDs->form->name);
prpRebuildTree(sDs);
} else if (strcasecmp(propName, "Caption") == 0) {
snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue);
if (sDs->formWin) {

BIN
apps/dvxbasic/noicon.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
apps/dvxbasic/samples/ICON32.BMP (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,18 @@
[Project]
Name = Multi Form Test 1
Author = Scott Duensing
Company = Kangaroo Punch Studios
Version = 1.00
Copyright = Copyright 2026 Scott Duensing
Description = Testing properties.
Icon = icon32.bmp
[Modules]
Count = 0
[Forms]
File0 = multi1.frm
Count = 1
[Settings]
StartupForm =

View file

@ -0,0 +1,10 @@
VERSION 1.00
Begin Form multi1
Caption = "Welcome to DVX BASIC!"
Layout = VBox
AutoSize = False
Resizable = True
Centered = True
Width = 400
Height = 300
End

View file

@ -69,7 +69,6 @@ static void doNew(void);
static void doOpen(void);
static void doSave(void);
static void doSaveAs(void);
static uint32_t hashText(const char *text);
static bool isDirty(void);
static void markClean(void);
static void onClose(WindowT *win);
@ -93,35 +92,18 @@ AppDescriptorT appDescriptor = {
// Dirty tracking
// ============================================================
// djb2-xor hash for dirty detection. Not cryptographic -- just a fast way
// to detect changes without storing a full copy of the last-saved text.
// False negatives are theoretically possible but vanishingly unlikely for
// text edits. This avoids the memory cost of keeping a shadow buffer.
static uint32_t hashText(const char *text) {
if (!text) {
return 0;
}
uint32_t h = 5381;
while (*text) {
h = ((h << 5) + h) ^ (uint8_t)*text;
text++;
}
return h;
}
// Dirty tracking uses dvxTextHash from dvxApp.h.
static bool isDirty(void) {
const char *text = wgtGetText(sTextArea);
return hashText(text) != sCleanHash;
return dvxTextHash(text) != sCleanHash;
}
static void markClean(void) {
const char *text = wgtGetText(sTextArea);
sCleanHash = hashText(text);
sCleanHash = dvxTextHash(text);
}

View file

@ -98,6 +98,7 @@ static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static WindowT *sPmWindow = NULL;
static WidgetT *sStatusLabel = NULL;
static PrefsHandleT *sPrefs = NULL;
static bool sMinOnRun = false;
static AppEntryT sAppFiles[MAX_APP_FILES];
static int32_t sAppCount = 0;
@ -341,8 +342,8 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
case CMD_MIN_ON_RUN:
sMinOnRun = !sMinOnRun;
shellEnsureConfigDir(sCtx);
prefsSetBool("options", "minimizeOnRun", sMinOnRun);
prefsSave();
prefsSetBool(sPrefs, "options", "minimizeOnRun", sMinOnRun);
prefsSave(sPrefs);
break;
case CMD_ABOUT:
@ -535,8 +536,8 @@ int32_t appMain(DxeAppContextT *ctx) {
// Load saved preferences
char prefsPath[DVX_MAX_PATH];
shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath));
prefsLoad(prefsPath);
sMinOnRun = prefsGetBool("options", "minimizeOnRun", false);
sPrefs = prefsLoad(prefsPath);
sMinOnRun = prefsGetBool(sPrefs, "options", "minimizeOnRun", false);
scanAppsDir();
buildPmWindow();

View file

@ -5,8 +5,8 @@
; Supported color depths: 8, 15, 16, 24, 32
[video]
width = 640
height = 480
width = 1024
height = 768
bpp = 16
; Mouse settings.

View file

@ -3783,6 +3783,26 @@ void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize
}
// ============================================================
// dvxTextHash
// ============================================================
uint32_t dvxTextHash(const char *text) {
if (!text) {
return 0;
}
uint32_t h = 5381;
while (*text) {
h = ((h << 5) + h) ^ (uint8_t)*text;
text++;
}
return h;
}
// ============================================================
// dvxColorLabel
// ============================================================
@ -4057,6 +4077,29 @@ void dvxFreeImage(uint8_t *data) {
}
// ============================================================
// dvxImageInfo
// ============================================================
bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) {
if (!path) {
return false;
}
int w = 0;
int h = 0;
int comp = 0;
if (stbi_info(path, &w, &h, &comp)) {
if (outW) { *outW = w; }
if (outH) { *outH = h; }
return true;
}
return false;
}
// ============================================================
// dvxGetBlitOps
// ============================================================

View file

@ -300,6 +300,10 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int
// Free a pixel buffer returned by dvxLoadImage.
void dvxFreeImage(uint8_t *data);
// Query image dimensions without decoding the full file.
// Returns true on success, false if the file can't be read.
bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH);
// Save native-format pixel data to a PNG file. The pixel data must be
// in the display's native format (as returned by dvxLoadImage or
// captured from a content buffer). Returns 0 on success, -1 on failure.
@ -338,4 +342,14 @@ bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t
// data size in bytes.
void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize);
// ============================================================
// Text hash for dirty tracking
// ============================================================
//
// djb2-xor hash for cheap dirty detection. Compare the hash at save
// time with the current hash to detect changes without keeping a
// shadow copy of the text. Not cryptographic.
uint32_t dvxTextHash(const char *text);
#endif // DVX_APP_H

View file

@ -1609,3 +1609,23 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha
return result;
}
// ============================================================
// dvxPromptSave
// ============================================================
int32_t dvxPromptSave(AppContextT *ctx, const char *title) {
int32_t result = dvxMessageBox(ctx, title ? title : "Save",
"Save changes?", MB_YESNOCANCEL | MB_ICONQUESTION);
if (result == ID_YES) {
return DVX_SAVE_YES;
}
if (result == ID_NO) {
return DVX_SAVE_NO;
}
return DVX_SAVE_CANCEL;
}

View file

@ -87,4 +87,20 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const
// Returns true if the user clicked OK, false if cancelled.
bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t step, int32_t *outVal);
// ============================================================
// Save prompt helper
// ============================================================
//
// Common "Save changes?" dialog for apps with unsaved data.
// Returns:
// DVX_SAVE_YES -- user wants to save (caller should save, then proceed)
// DVX_SAVE_NO -- user wants to discard (caller should proceed without saving)
// DVX_SAVE_CANCEL -- user cancelled (caller should abort the operation)
#define DVX_SAVE_YES 1
#define DVX_SAVE_NO 2
#define DVX_SAVE_CANCEL 3
int32_t dvxPromptSave(AppContextT *ctx, const char *title);
#endif // DVX_DIALOG_H

View file

@ -1,8 +1,7 @@
// dvxPrefs.c -- INI-based preferences system (read/write)
//
// Custom INI parser and writer. Stores entries as a dynamic array of
// section/key/value triples using stb_ds. Preserves insertion order
// on save so the file remains human-readable.
// Handle-based: each PrefsHandleT holds its own entry array and file
// path. Multiple INI files can be open simultaneously.
#include "dvxPrefs.h"
@ -12,7 +11,6 @@
#include <string.h>
#include "dvxMem.h"
// stb_ds dynamic arrays (implementation lives in libtasks.a)
#include "thirdparty/stb_ds_wrap.h"
@ -26,12 +24,14 @@ typedef struct {
char *value;
} PrefsEntryT;
// Comment lines are stored to preserve them on save. A comment has
// Comment lines are stored to preserve them on save. A comment has
// key=NULL and value=the full line text (including the ; prefix).
// Section headers have key=NULL and value=NULL.
static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array
static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave)
struct PrefsHandleT {
PrefsEntryT *entries; // stb_ds dynamic array
char *filePath; // path used by prefsSave
};
// ============================================================
@ -64,7 +64,6 @@ static void freeEntry(PrefsEntryT *e) {
}
// Case-insensitive string compare
static int strcmpci(const char *a, const char *b) {
for (;;) {
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
@ -79,10 +78,9 @@ static int strcmpci(const char *a, const char *b) {
}
// Find an entry by section+key (case-insensitive). Returns index or -1.
static int32_t findEntry(const char *section, const char *key) {
for (int32_t i = 0; i < arrlen(sEntries); i++) {
PrefsEntryT *e = &sEntries[i];
static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) {
for (int32_t i = 0; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
if (e->key && e->section &&
strcmpci(e->section, section) == 0 &&
@ -95,10 +93,9 @@ static int32_t findEntry(const char *section, const char *key) {
}
// Find the index of a section header entry. Returns -1 if not found.
static int32_t findSection(const char *section) {
for (int32_t i = 0; i < arrlen(sEntries); i++) {
PrefsEntryT *e = &sEntries[i];
static int32_t findSection(PrefsHandleT *h, const char *section) {
for (int32_t i = 0; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
if (!e->key && !e->value && e->section &&
strcmpci(e->section, section) == 0) {
@ -110,7 +107,6 @@ static int32_t findSection(const char *section) {
}
// Trim leading/trailing whitespace in place. Returns pointer into buf.
static char *trimInPlace(char *buf) {
while (*buf == ' ' || *buf == '\t') {
buf++;
@ -127,18 +123,31 @@ static char *trimInPlace(char *buf) {
// ============================================================
// prefsFree
// prefsClose
// ============================================================
void prefsFree(void) {
for (int32_t i = 0; i < arrlen(sEntries); i++) {
freeEntry(&sEntries[i]);
void prefsClose(PrefsHandleT *h) {
if (!h) {
return;
}
arrfree(sEntries);
sEntries = NULL;
free(sFilePath);
sFilePath = NULL;
for (int32_t i = 0; i < arrlen(h->entries); i++) {
freeEntry(&h->entries[i]);
}
arrfree(h->entries);
free(h->filePath);
free(h);
}
// ============================================================
// prefsCreate
// ============================================================
PrefsHandleT *prefsCreate(void) {
PrefsHandleT *h = (PrefsHandleT *)calloc(1, sizeof(PrefsHandleT));
return h;
}
@ -146,8 +155,8 @@ void prefsFree(void) {
// prefsGetBool
// ============================================================
bool prefsGetBool(const char *section, const char *key, bool defaultVal) {
const char *val = prefsGetString(section, key, NULL);
bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal) {
const char *val = prefsGetString(h, section, key, NULL);
if (!val) {
return defaultVal;
@ -171,8 +180,8 @@ bool prefsGetBool(const char *section, const char *key, bool defaultVal) {
// prefsGetInt
// ============================================================
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) {
const char *val = prefsGetString(section, key, NULL);
int32_t prefsGetInt(PrefsHandleT *h, const char *section, const char *key, int32_t defaultVal) {
const char *val = prefsGetString(h, section, key, NULL);
if (!val) {
return defaultVal;
@ -193,14 +202,18 @@ int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) {
// prefsGetString
// ============================================================
const char *prefsGetString(const char *section, const char *key, const char *defaultVal) {
int32_t idx = findEntry(section, key);
const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal) {
if (!h) {
return defaultVal;
}
int32_t idx = findEntry(h, section, key);
if (idx < 0) {
return defaultVal;
}
return sEntries[idx].value;
return h->entries[idx].value;
}
@ -208,24 +221,25 @@ const char *prefsGetString(const char *section, const char *key, const char *def
// prefsLoad
// ============================================================
bool prefsLoad(const char *filename) {
prefsFree();
PrefsHandleT *prefsLoad(const char *filename) {
PrefsHandleT *h = prefsCreate();
// Always store the path so prefsSave can create the file
// even if it doesn't exist yet.
sFilePath = dupStr(filename);
if (!h) {
return NULL;
}
h->filePath = dupStr(filename);
FILE *fp = fopen(filename, "rb");
if (!fp) {
return false;
return h;
}
char line[512];
char *currentSection = dupStr("");
while (fgets(line, sizeof(line), fp)) {
// Strip trailing whitespace/newline
char *end = line + strlen(line) - 1;
while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) {
@ -234,30 +248,26 @@ bool prefsLoad(const char *filename) {
char *p = line;
// Skip leading whitespace
while (*p == ' ' || *p == '\t') {
p++;
}
// Blank line -- store as comment to preserve formatting
if (*p == '\0') {
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
e.value = dupStr("");
arrput(sEntries, e);
arrput(h->entries, e);
continue;
}
// Comment line
if (*p == ';' || *p == '#') {
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
e.value = dupStr(line);
arrput(sEntries, e);
arrput(h->entries, e);
continue;
}
// Section header
if (*p == '[') {
char *close = strchr(p, ']');
@ -268,13 +278,12 @@ bool prefsLoad(const char *filename) {
PrefsEntryT e = {0};
e.section = dupStr(currentSection);
arrput(sEntries, e);
arrput(h->entries, e);
}
continue;
}
// Key=value
char *eq = strchr(p, '=');
if (eq) {
@ -284,13 +293,13 @@ bool prefsLoad(const char *filename) {
e.section = dupStr(currentSection);
e.key = dupStr(trimInPlace(p));
e.value = dupStr(trimInPlace(eq + 1));
arrput(sEntries, e);
arrput(h->entries, e);
}
}
free(currentSection);
fclose(fp);
return true;
return h;
}
@ -298,12 +307,16 @@ bool prefsLoad(const char *filename) {
// prefsRemove
// ============================================================
void prefsRemove(const char *section, const char *key) {
int32_t idx = findEntry(section, key);
void prefsRemove(PrefsHandleT *h, const char *section, const char *key) {
if (!h) {
return;
}
int32_t idx = findEntry(h, section, key);
if (idx >= 0) {
freeEntry(&sEntries[idx]);
arrdel(sEntries, idx);
freeEntry(&h->entries[idx]);
arrdel(h->entries, idx);
}
}
@ -312,12 +325,12 @@ void prefsRemove(const char *section, const char *key) {
// prefsSave
// ============================================================
bool prefsSave(void) {
if (!sFilePath) {
bool prefsSave(PrefsHandleT *h) {
if (!h || !h->filePath) {
return false;
}
return prefsSaveAs(sFilePath);
return prefsSaveAs(h, h->filePath);
}
@ -325,29 +338,30 @@ bool prefsSave(void) {
// prefsSaveAs
// ============================================================
bool prefsSaveAs(const char *filename) {
bool prefsSaveAs(PrefsHandleT *h, const char *filename) {
if (!h) {
return false;
}
FILE *fp = fopen(filename, "wb");
if (!fp) {
return false;
}
for (int32_t i = 0; i < arrlen(sEntries); i++) {
PrefsEntryT *e = &sEntries[i];
for (int32_t i = 0; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
// Comment or blank line (key=NULL, value=text or empty)
if (!e->key && e->value) {
fprintf(fp, "%s\r\n", e->value);
continue;
}
// Section header (key=NULL, value=NULL)
if (!e->key && !e->value) {
fprintf(fp, "[%s]\r\n", e->section);
continue;
}
// Key=value
if (e->key && e->value) {
fprintf(fp, "%s = %s\r\n", e->key, e->value);
}
@ -362,8 +376,8 @@ bool prefsSaveAs(const char *filename) {
// prefsSetBool
// ============================================================
void prefsSetBool(const char *section, const char *key, bool value) {
prefsSetString(section, key, value ? "true" : "false");
void prefsSetBool(PrefsHandleT *h, const char *section, const char *key, bool value) {
prefsSetString(h, section, key, value ? "true" : "false");
}
@ -371,10 +385,10 @@ void prefsSetBool(const char *section, const char *key, bool value) {
// prefsSetInt
// ============================================================
void prefsSetInt(const char *section, const char *key, int32_t value) {
void prefsSetInt(PrefsHandleT *h, const char *section, const char *key, int32_t value) {
char buf[32];
snprintf(buf, sizeof(buf), "%ld", (long)value);
prefsSetString(section, key, buf);
prefsSetString(h, section, key, buf);
}
@ -382,48 +396,45 @@ void prefsSetInt(const char *section, const char *key, int32_t value) {
// prefsSetString
// ============================================================
void prefsSetString(const char *section, const char *key, const char *value) {
int32_t idx = findEntry(section, key);
if (idx >= 0) {
// Update existing entry
free(sEntries[idx].value);
sEntries[idx].value = dupStr(value);
void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value) {
if (!h) {
return;
}
// Find or create section header
int32_t secIdx = findSection(section);
int32_t idx = findEntry(h, section, key);
if (idx >= 0) {
free(h->entries[idx].value);
h->entries[idx].value = dupStr(value);
return;
}
int32_t secIdx = findSection(h, section);
if (secIdx < 0) {
// Add blank line before new section (unless file is empty)
if (arrlen(sEntries) > 0) {
if (arrlen(h->entries) > 0) {
PrefsEntryT blank = {0};
blank.section = dupStr(section);
blank.value = dupStr("");
arrput(sEntries, blank);
arrput(h->entries, blank);
}
// Add section header
PrefsEntryT secEntry = {0};
secEntry.section = dupStr(section);
arrput(sEntries, secEntry);
secIdx = arrlen(sEntries) - 1;
arrput(h->entries, secEntry);
secIdx = arrlen(h->entries) - 1;
}
// Find insertion point: after last entry in this section
int32_t insertAt = secIdx + 1;
while (insertAt < arrlen(sEntries)) {
PrefsEntryT *e = &sEntries[insertAt];
while (insertAt < arrlen(h->entries)) {
PrefsEntryT *e = &h->entries[insertAt];
// Stop if we've hit a different section header
if (!e->key && !e->value && e->section &&
strcmpci(e->section, section) != 0) {
break;
}
// Stop if we've hit an entry from a different section
if (e->section && strcmpci(e->section, section) != 0) {
break;
}
@ -431,10 +442,9 @@ void prefsSetString(const char *section, const char *key, const char *value) {
insertAt++;
}
// Insert new entry
PrefsEntryT newEntry = {0};
newEntry.section = dupStr(section);
newEntry.key = dupStr(key);
newEntry.value = dupStr(value);
arrins(sEntries, insertAt, newEntry);
arrins(h->entries, insertAt, newEntry);
}

View file

@ -1,9 +1,8 @@
// dvxPrefs.h -- INI-based preferences system (read/write)
//
// Loads a configuration file at startup and provides typed accessors
// with caller-supplied defaults. Values can be modified at runtime
// and saved back to disk. If the file is missing or a key is absent,
// getters return the default silently.
// Handle-based API: multiple INI files can be open simultaneously.
// Each prefsOpen/prefsLoad returns a handle that must be passed to
// all subsequent calls and freed with prefsClose when done.
#ifndef DVX_PREFS_H
#define DVX_PREFS_H
@ -11,41 +10,47 @@
#include <stdbool.h>
#include <stdint.h>
// Load an INI file into memory. Returns true on success, false if the
// file could not be opened (all getters will return their defaults).
// Only one file may be loaded at a time; calling again frees the previous.
bool prefsLoad(const char *filename);
// Opaque handle to a loaded preferences file.
typedef struct PrefsHandleT PrefsHandleT;
// Save the current in-memory state back to the file that was loaded.
// Returns true on success.
bool prefsSave(void);
// Create an empty preferences handle (no file loaded). Useful for
// building a new INI from scratch before saving.
PrefsHandleT *prefsCreate(void);
// Save the current in-memory state to a specific file.
bool prefsSaveAs(const char *filename);
// Load an INI file into a new handle. Returns NULL on allocation
// failure. If the file doesn't exist, returns a valid empty handle
// (all getters return defaults) with the path stored for prefsSave.
PrefsHandleT *prefsLoad(const char *filename);
// Release all memory held by the preferences.
void prefsFree(void);
// Save the in-memory state back to the file that was loaded.
bool prefsSave(PrefsHandleT *h);
// Retrieve a string value. Returns defaultVal if the key is not present.
// The returned pointer is valid until the key is modified or prefsFree().
const char *prefsGetString(const char *section, const char *key, const char *defaultVal);
// Save the in-memory state to a specific file.
bool prefsSaveAs(PrefsHandleT *h, const char *filename);
// Release all memory held by the handle.
void prefsClose(PrefsHandleT *h);
// Retrieve a string value. Returns defaultVal if the key is not present.
// The returned pointer is valid until the key is modified or prefsClose.
const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal);
// Retrieve an integer value.
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal);
int32_t prefsGetInt(PrefsHandleT *h, const char *section, const char *key, int32_t defaultVal);
// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0".
bool prefsGetBool(const char *section, const char *key, bool defaultVal);
// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0".
bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal);
// Set a string value. Creates the section and key if they don't exist.
void prefsSetString(const char *section, const char *key, const char *value);
// Set a string value. Creates the section and key if they don't exist.
void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value);
// Set an integer value.
void prefsSetInt(const char *section, const char *key, int32_t value);
void prefsSetInt(PrefsHandleT *h, const char *section, const char *key, int32_t value);
// Set a boolean value (stored as "true"/"false").
void prefsSetBool(const char *section, const char *key, bool value);
void prefsSetBool(PrefsHandleT *h, const char *section, const char *key, bool value);
// Remove a key from a section. No-op if not found.
void prefsRemove(const char *section, const char *key);
// Remove a key from a section. No-op if not found.
void prefsRemove(PrefsHandleT *h, const char *section, const char *key);
#endif

3
core/thirdparty/stb_ds_impl.c vendored Normal file
View file

@ -0,0 +1,3 @@
// stb_ds_impl.c -- stb_ds implementation for host test builds
#define STB_DS_IMPLEMENTATION
#include "stb_ds.h"

View file

@ -47,6 +47,7 @@
// ============================================================
static AppContextT sCtx;
static PrefsHandleT *sPrefs = NULL;
// setjmp buffer for crash recovery. The crash handler longjmps here to
// return control to the shell's main loop after an app crashes.
static jmp_buf sCrashJmp;
@ -211,11 +212,11 @@ int shellMain(int argc, char *argv[]) {
dvxLog("DVX Shell starting...");
// Load preferences (missing file or keys silently use defaults)
prefsLoad("CONFIG/DVX.INI");
sPrefs = prefsLoad("CONFIG/DVX.INI");
int32_t videoW = prefsGetInt("video", "width", 640);
int32_t videoH = prefsGetInt("video", "height", 480);
int32_t videoBpp = prefsGetInt("video", "bpp", 16);
int32_t videoW = prefsGetInt(sPrefs, "video", "width", 640);
int32_t videoH = prefsGetInt(sPrefs, "video", "height", 480);
int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp", 16);
dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp);
// Initialize GUI
@ -223,13 +224,13 @@ int shellMain(int argc, char *argv[]) {
if (result == 0) {
// Apply mouse preferences
const char *wheelStr = prefsGetString("mouse", "wheel", "normal");
const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", "normal");
int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1;
int32_t dblClick = prefsGetInt("mouse", "doubleclick", 500);
int32_t dblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", 500);
// Map acceleration name to double-speed threshold (mickeys/sec).
// "off" sets a very high threshold so acceleration never triggers.
const char *accelStr = prefsGetString("mouse", "acceleration", "medium");
const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium");
int32_t accelVal = 0;
if (strcmp(accelStr, "off") == 0) {
@ -249,7 +250,7 @@ int shellMain(int argc, char *argv[]) {
bool colorsLoaded = false;
for (int32_t i = 0; i < ColorCountE; i++) {
const char *val = prefsGetString("colors", dvxColorName((ColorIdE)i), NULL);
const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL);
if (val) {
int r;
@ -271,7 +272,7 @@ int shellMain(int argc, char *argv[]) {
}
// Apply saved wallpaper mode and image
const char *wpMode = prefsGetString("desktop", "mode", "stretch");
const char *wpMode = prefsGetString(sPrefs, "desktop", "mode", "stretch");
if (strcmp(wpMode, "tile") == 0) {
sCtx.wallpaperMode = WallpaperTileE;
@ -281,7 +282,7 @@ int shellMain(int argc, char *argv[]) {
sCtx.wallpaperMode = WallpaperStretchE;
}
const char *wpPath = prefsGetString("desktop", "wallpaper", NULL);
const char *wpPath = prefsGetString(sPrefs, "desktop", "wallpaper", NULL);
if (wpPath) {
if (dvxSetWallpaper(&sCtx, wpPath)) {
@ -343,7 +344,7 @@ int shellMain(int argc, char *argv[]) {
platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog);
// Load the desktop app (configurable via [shell] desktop= in dvx.ini)
const char *desktopApp = prefsGetString("shell", "desktop", SHELL_DESKTOP_APP);
const char *desktopApp = prefsGetString(sPrefs, "shell", "desktop", SHELL_DESKTOP_APP);
int32_t desktopId = shellLoadApp(&sCtx, desktopApp);
if (desktopId < 0) {
@ -439,7 +440,8 @@ int shellMain(int argc, char *argv[]) {
tsShutdown();
dvxShutdown(&sCtx);
prefsFree();
prefsClose(sPrefs);
sPrefs = NULL;
dvxLog("DVX Shell exited.");
return 0;

View file

@ -10,11 +10,14 @@ BINDIR = ../bin
.PHONY: all clean
all: $(BINDIR)/dvxres $(BINDIR)/mktbicon
all: $(BINDIR)/dvxres $(BINDIR)/mkicon $(BINDIR)/mktbicon
$(BINDIR)/dvxres: dvxres.c ../core/dvxResource.c ../core/dvxResource.h | $(BINDIR)
$(CC) $(CFLAGS) -o $@ dvxres.c ../core/dvxResource.c
$(BINDIR)/mkicon: mkicon.c | $(BINDIR)
$(CC) $(CFLAGS) -o $@ mkicon.c -lm
$(BINDIR)/mktbicon: mktbicon.c | $(BINDIR)
$(CC) $(CFLAGS) -o $@ mktbicon.c

View file

@ -62,6 +62,36 @@ static void vline(int x, int y0, int h, uint8_t r, uint8_t g, uint8_t b) {
}
}
// No-icon placeholder: grey square with red diagonal X
static void iconNoIcon(void) {
clear(192, 192, 192);
// Border
rect(0, 0, 32, 1, 128, 128, 128);
rect(0, 31, 32, 1, 128, 128, 128);
rect(0, 0, 1, 32, 128, 128, 128);
rect(31, 0, 1, 32, 128, 128, 128);
// Red X (two diagonal lines, 2px thick)
for (int i = 4; i < 28; i++) {
pixel(i, i, 200, 40, 40);
pixel(i + 1, i, 200, 40, 40);
pixel(31 - i, i, 200, 40, 40);
pixel(30 - i, i, 200, 40, 40);
}
// "?" in the center
for (int x = 13; x <= 18; x++) { pixel(x, 8, 80, 80, 80); }
pixel(19, 9, 80, 80, 80);
pixel(19, 10, 80, 80, 80);
for (int x = 15; x <= 18; x++) { pixel(x, 11, 80, 80, 80); }
pixel(15, 12, 80, 80, 80);
pixel(15, 13, 80, 80, 80);
pixel(15, 15, 80, 80, 80);
pixel(16, 15, 80, 80, 80);
}
// Clock icon: circle with hands
static void iconClock(void) {
clear(192, 192, 192);
@ -294,14 +324,16 @@ static void writeBmp(const char *path) {
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "Usage: mkicon <output.bmp> <type>\n");
fprintf(stderr, "Types: clock, notepad, cpanel, dvxdemo, imgview, basic\n");
fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic\n");
return 1;
}
const char *path = argv[1];
const char *type = argv[2];
if (strcmp(type, "clock") == 0) {
if (strcmp(type, "noicon") == 0) {
iconNoIcon();
} else if (strcmp(type, "clock") == 0) {
iconClock();
} else if (strcmp(type, "notepad") == 0) {
iconNotepad();