DVX_GUI/widgets/dataCtrl/widgetDataCtrl.c
2026-04-04 20:00:25 -05:00

577 lines
16 KiB
C

#define DVX_WIDGET_IMPL
// widgetDataCtrl.c -- VB3-style Data control for database binding
//
// A visible navigation bar that connects to a SQLite database via
// dvxSql* functions. Reads all rows from the RecordSource query
// into an in-memory cache for bidirectional navigation. Fires
// Reposition events when the cursor moves so bound controls can
// update.
//
// The control resolves dvxSql* functions via dlsym at first use,
// keeping the widget DXE independent of the SQL library DXE at
// link time (both are loaded by the same loader, so dlsym works).
#include "dvxWidgetPlugin.h"
#include "thirdparty/stb_ds_wrap.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Constants
// ============================================================
#define DATA_HEIGHT 24
#define DATA_BTN_W 20
#define DATA_BORDER 2
#define DATA_MAX_COLS 64
#define DATA_MAX_FIELD 256
// ============================================================
// Per-row cache entry
// ============================================================
typedef struct {
char **fields; // array of strdup'd strings, one per column
int32_t fieldCount;
} DataRowT;
// ============================================================
// Per-instance data
// ============================================================
typedef struct {
char databaseName[DATA_MAX_FIELD];
char recordSource[DATA_MAX_FIELD];
char caption[DATA_MAX_FIELD];
// Cached result set
DataRowT *rows; // stb_ds dynamic array
int32_t rowCount;
char **colNames; // stb_ds dynamic array of strdup'd names
int32_t colCount;
int32_t currentRow; // 0-based, -1 = no rows
bool bof;
bool eof;
// SQL function pointers (resolved via dlsym)
int32_t (*sqlOpen)(const char *);
void (*sqlClose)(int32_t);
int32_t (*sqlQuery)(int32_t, const char *);
bool (*sqlNext)(int32_t);
bool (*sqlEof)(int32_t);
int32_t (*sqlFieldCount)(int32_t);
const char *(*sqlFieldName)(int32_t, int32_t);
const char *(*sqlFieldText)(int32_t, int32_t);
void (*sqlFreeResult)(int32_t);
bool sqlResolved;
} DataCtrlDataT;
static int32_t sTypeId = -1;
// ============================================================
// resolveSql -- lazily resolve dvxSql* function pointers
// ============================================================
static void resolveSql(DataCtrlDataT *d) {
if (d->sqlResolved) {
return;
}
d->sqlOpen = (int32_t (*)(const char *))dlsym(NULL, "_dvxSqlOpen");
d->sqlClose = (void (*)(int32_t))dlsym(NULL, "_dvxSqlClose");
d->sqlQuery = (int32_t (*)(int32_t, const char *))dlsym(NULL, "_dvxSqlQuery");
d->sqlNext = (bool (*)(int32_t))dlsym(NULL, "_dvxSqlNext");
d->sqlEof = (bool (*)(int32_t))dlsym(NULL, "_dvxSqlEof");
d->sqlFieldCount = (int32_t (*)(int32_t))dlsym(NULL, "_dvxSqlFieldCount");
d->sqlFieldName = (const char *(*)(int32_t, int32_t))dlsym(NULL, "_dvxSqlFieldName");
d->sqlFieldText = (const char *(*)(int32_t, int32_t))dlsym(NULL, "_dvxSqlFieldText");
d->sqlFreeResult = (void (*)(int32_t))dlsym(NULL, "_dvxSqlFreeResult");
d->sqlResolved = true;
}
// ============================================================
// freeCache -- release the cached result set
// ============================================================
static void freeCache(DataCtrlDataT *d) {
for (int32_t i = 0; i < d->rowCount; i++) {
for (int32_t j = 0; j < d->rows[i].fieldCount; j++) {
free(d->rows[i].fields[j]);
}
free(d->rows[i].fields);
}
arrfree(d->rows);
d->rows = NULL;
d->rowCount = 0;
for (int32_t i = 0; i < d->colCount; i++) {
free(d->colNames[i]);
}
arrfree(d->colNames);
d->colNames = NULL;
d->colCount = 0;
d->currentRow = -1;
d->bof = true;
d->eof = true;
}
// ============================================================
// dataCtrlRefresh -- execute query and cache all rows
// ============================================================
void dataCtrlRefresh(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
resolveSql(d);
freeCache(d);
if (!d->sqlOpen || !d->databaseName[0] || !d->recordSource[0]) {
return;
}
int32_t db = d->sqlOpen(d->databaseName);
if (db <= 0) {
return;
}
// If recordSource doesn't start with SELECT, wrap it as "SELECT * FROM table"
char query[512];
if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) {
snprintf(query, sizeof(query), "%s", d->recordSource);
} else {
snprintf(query, sizeof(query), "SELECT * FROM %s", d->recordSource);
}
int32_t rs = d->sqlQuery(db, query);
if (rs <= 0) {
d->sqlClose(db);
return;
}
// Cache column names
d->colCount = d->sqlFieldCount(rs);
for (int32_t i = 0; i < d->colCount; i++) {
const char *name = d->sqlFieldName(rs, i);
arrput(d->colNames, strdup(name ? name : ""));
}
// Cache all rows
while (d->sqlNext(rs)) {
DataRowT row;
row.fieldCount = d->colCount;
row.fields = (char **)malloc(d->colCount * sizeof(char *));
for (int32_t i = 0; i < d->colCount; i++) {
const char *text = d->sqlFieldText(rs, i);
row.fields[i] = strdup(text ? text : "");
}
arrput(d->rows, row);
}
d->rowCount = (int32_t)arrlen(d->rows);
d->sqlFreeResult(rs);
d->sqlClose(db);
if (d->rowCount > 0) {
d->currentRow = 0;
d->bof = false;
d->eof = false;
}
// Auto-caption
if (!d->caption[0]) {
snprintf(d->caption, sizeof(d->caption), "%s", d->recordSource);
}
wgtInvalidatePaint(w);
}
// ============================================================
// Navigation methods
// ============================================================
static void fireReposition(WidgetT *w) {
wgtInvalidatePaint(w);
if (w->onChange) {
w->onChange(w);
}
}
static void dataCtrlMoveFirst(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d->rowCount <= 0) {
return;
}
d->currentRow = 0;
d->bof = false;
d->eof = false;
fireReposition(w);
}
static void dataCtrlMovePrev(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d->rowCount <= 0 || d->currentRow <= 0) {
d->bof = true;
return;
}
d->currentRow--;
d->bof = (d->currentRow == 0);
d->eof = false;
fireReposition(w);
}
static void dataCtrlMoveNext(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d->rowCount <= 0 || d->currentRow >= d->rowCount - 1) {
d->eof = true;
return;
}
d->currentRow++;
d->bof = false;
d->eof = (d->currentRow >= d->rowCount - 1);
fireReposition(w);
}
static void dataCtrlMoveLast(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d->rowCount <= 0) {
return;
}
d->currentRow = d->rowCount - 1;
d->bof = false;
d->eof = false;
fireReposition(w);
}
// ============================================================
// Public accessor: get field value from current row by column name
// ============================================================
// Exported for form runtime data binding (resolved via dlsym)
const char *dataCtrlGetField(WidgetT *w, const char *colName) {
if (!w || !w->data || !colName) {
return "";
}
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d->currentRow < 0 || d->currentRow >= d->rowCount) {
return "";
}
for (int32_t i = 0; i < d->colCount; i++) {
if (strcasecmp(d->colNames[i], colName) == 0) {
return d->rows[d->currentRow].fields[i];
}
}
return "";
}
// ============================================================
// Paint
// ============================================================
static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
w->calcMinW = DATA_BTN_W * 4 + 60; // 4 buttons + some caption space
w->calcMinH = DATA_HEIGHT;
}
static void dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
uint32_t face = colors->buttonFace;
uint32_t fg = colors->contentFg;
uint32_t hi = colors->windowHighlight;
uint32_t sh = colors->windowShadow;
// Background
rectFill(disp, ops, w->x, w->y, w->w, w->h, face);
// Outer bevel
BevelStyleT bevel;
bevel.highlight = hi;
bevel.shadow = sh;
bevel.face = face;
bevel.width = DATA_BORDER;
drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);
int32_t innerX = w->x + DATA_BORDER;
int32_t innerY = w->y + DATA_BORDER;
int32_t innerW = w->w - DATA_BORDER * 2;
int32_t innerH = w->h - DATA_BORDER * 2;
// Button zones
int32_t btnH = innerH;
int32_t btnY = innerY;
// |< button (MoveFirst)
BevelStyleT btnBevel = { hi, sh, face, 1 };
drawBevel(disp, ops, innerX, btnY, DATA_BTN_W, btnH, &btnBevel);
// Draw |< glyph
int32_t cx = innerX + DATA_BTN_W / 2;
int32_t cy = btnY + btnH / 2;
drawVLine(disp, ops, cx - 3, cy - 4, 9, fg);
for (int32_t i = 0; i < 4; i++) {
drawHLine(disp, ops, cx - 1 + i, cy - i, 1, fg);
drawHLine(disp, ops, cx - 1 + i, cy + i, 1, fg);
}
// < button (MovePrev)
drawBevel(disp, ops, innerX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel);
cx = innerX + DATA_BTN_W + DATA_BTN_W / 2;
for (int32_t i = 0; i < 4; i++) {
drawHLine(disp, ops, cx + i, cy - i, 1, fg);
drawHLine(disp, ops, cx + i, cy + i, 1, fg);
}
// > button (MoveNext)
int32_t rightX = innerX + innerW - DATA_BTN_W * 2;
drawBevel(disp, ops, rightX, btnY, DATA_BTN_W, btnH, &btnBevel);
cx = rightX + DATA_BTN_W / 2;
for (int32_t i = 0; i < 4; i++) {
drawHLine(disp, ops, cx - i, cy - i, 1, fg);
drawHLine(disp, ops, cx - i, cy + i, 1, fg);
}
// >| button (MoveLast)
drawBevel(disp, ops, rightX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel);
cx = rightX + DATA_BTN_W + DATA_BTN_W / 2;
drawVLine(disp, ops, cx + 3, cy - 4, 9, fg);
for (int32_t i = 0; i < 4; i++) {
drawHLine(disp, ops, cx - i, cy - i, 1, fg);
drawHLine(disp, ops, cx - i, cy + i, 1, fg);
}
// Caption in center
int32_t captionX = innerX + DATA_BTN_W * 2 + 4;
int32_t captionW = innerW - DATA_BTN_W * 4 - 8;
const char *text = d->caption[0] ? d->caption : d->recordSource;
if (text[0] && captionW > 0) {
int32_t tw = textWidth(font, text);
int32_t tx = captionX + (captionW - tw) / 2;
int32_t ty = innerY + (innerH - font->charHeight) / 2;
drawText(disp, ops, font, tx, ty, text, fg, face, false);
}
}
// ============================================================
// Mouse handler
// ============================================================
static void dataCtrlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vy;
sFocusedWidget = w;
int32_t innerX = w->x + DATA_BORDER;
int32_t innerW = w->w - DATA_BORDER * 2;
int32_t relX = vx - innerX;
if (relX < DATA_BTN_W) {
dataCtrlMoveFirst(w);
} else if (relX < DATA_BTN_W * 2) {
dataCtrlMovePrev(w);
} else if (relX >= innerW - DATA_BTN_W) {
dataCtrlMoveLast(w);
} else if (relX >= innerW - DATA_BTN_W * 2) {
dataCtrlMoveNext(w);
}
}
// ============================================================
// Property getters/setters
// ============================================================
static const char *dataCtrlGetDatabaseName(const WidgetT *w) {
return ((DataCtrlDataT *)w->data)->databaseName;
}
static void dataCtrlSetDatabaseName(WidgetT *w, const char *val) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
snprintf(d->databaseName, sizeof(d->databaseName), "%s", val ? val : "");
}
static const char *dataCtrlGetRecordSource(const WidgetT *w) {
return ((DataCtrlDataT *)w->data)->recordSource;
}
static void dataCtrlSetRecordSource(WidgetT *w, const char *val) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
snprintf(d->recordSource, sizeof(d->recordSource), "%s", val ? val : "");
}
static const char *dataCtrlGetCaption(const WidgetT *w) {
return ((DataCtrlDataT *)w->data)->caption;
}
static void dataCtrlSetCaption(WidgetT *w, const char *val) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
snprintf(d->caption, sizeof(d->caption), "%s", val ? val : "");
wgtInvalidatePaint(w);
}
static bool dataCtrlGetBof(const WidgetT *w) {
return ((DataCtrlDataT *)w->data)->bof;
}
static bool dataCtrlGetEof(const WidgetT *w) {
return ((DataCtrlDataT *)w->data)->eof;
}
// ============================================================
// Destroy
// ============================================================
static void dataCtrlDestroy(WidgetT *w) {
DataCtrlDataT *d = (DataCtrlDataT *)w->data;
if (d) {
freeCache(d);
free(d);
}
}
// ============================================================
// DXE registration
// ============================================================
static const WidgetClassT sClass = {
.version = WGT_CLASS_VERSION,
.flags = WCLASS_FOCUSABLE,
.handlers = {
[WGT_METHOD_PAINT] = (void *)dataCtrlPaint,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)dataCtrlCalcMinSize,
[WGT_METHOD_ON_MOUSE] = (void *)dataCtrlOnMouse,
[WGT_METHOD_DESTROY] = (void *)dataCtrlDestroy,
}
};
static WidgetT *dataCtrlCreate(WidgetT *parent) {
if (!parent) {
return NULL;
}
WidgetT *w = widgetAlloc(parent, sTypeId);
if (w) {
DataCtrlDataT *d = (DataCtrlDataT *)calloc(1, sizeof(DataCtrlDataT));
if (d) {
d->currentRow = -1;
d->bof = true;
d->eof = true;
}
w->data = d;
w->minH = wgtPixels(DATA_HEIGHT);
}
return w;
}
static const struct {
WidgetT *(*create)(WidgetT *parent);
void (*refresh)(WidgetT *w);
void (*moveFirst)(WidgetT *w);
void (*movePrev)(WidgetT *w);
void (*moveNext)(WidgetT *w);
void (*moveLast)(WidgetT *w);
const char *(*getField)(WidgetT *w, const char *colName);
} sApi = {
.create = dataCtrlCreate,
.refresh = dataCtrlRefresh,
.moveFirst = dataCtrlMoveFirst,
.movePrev = dataCtrlMovePrev,
.moveNext = dataCtrlMoveNext,
.moveLast = dataCtrlMoveLast,
.getField = dataCtrlGetField,
};
static const WgtPropDescT sProps[] = {
{ "DatabaseName", WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName, (void *)dataCtrlSetDatabaseName, NULL },
{ "RecordSource", WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource, (void *)dataCtrlSetRecordSource, NULL },
{ "Caption", WGT_IFACE_STRING, (void *)dataCtrlGetCaption, (void *)dataCtrlSetCaption, NULL },
{ "BOF", WGT_IFACE_BOOL, (void *)dataCtrlGetBof, NULL, NULL },
{ "EOF", WGT_IFACE_BOOL, (void *)dataCtrlGetEof, NULL, NULL },
};
static const WgtMethodDescT sMethods[] = {
{ "Refresh", WGT_SIG_VOID, (void *)dataCtrlRefresh },
{ "MoveFirst", WGT_SIG_VOID, (void *)dataCtrlMoveFirst },
{ "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev },
{ "MoveNext", WGT_SIG_VOID, (void *)dataCtrlMoveNext },
{ "MoveLast", WGT_SIG_VOID, (void *)dataCtrlMoveLast },
};
static const WgtEventDescT sEvents[] = {
{ "Reposition" },
};
static const WgtIfaceT sIface = {
.basName = "Data",
.props = sProps,
.propCount = 5,
.methods = sMethods,
.methodCount = 5,
.events = sEvents,
.eventCount = 1,
.createSig = WGT_CREATE_PARENT,
.isContainer = false,
.defaultEvent = "Reposition",
.namePrefix = "Data",
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClass);
wgtRegisterApi("data", &sApi);
wgtRegisterIface("data", &sIface);
}