DVX_GUI/widgets/dbGrid/widgetDbGrid.c

1221 lines
34 KiB
C

#define DVX_WIDGET_IMPL
// widgetDbGrid.c -- Database grid widget for tabular record display
//
// A read-only grid that displays all records from a Data control in a
// scrollable, sortable table. Columns auto-populate from the Data
// control's column names and can be hidden, resized, and reordered
// by clicking headers. Selecting a row syncs the Data control's
// cursor position.
//
// The grid reads directly from the Data control's cached rows via
// the row-level accessor API (getRowCount, getCellText, etc.) so
// there is no separate copy of the data. The grid is refreshed by
// calling dbGridRefresh() which re-reads the Data control's state.
//
// Depends on the datactrl widget DXE for the DataCtrlApiT.
#include "dvxWgtP.h"
#include "../dataCtrl/dataCtrl.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Constants
// ============================================================
#define DBGRID_BORDER 2
#define DBGRID_MAX_COLS 32
#define DBGRID_MIN_COL_W 20
#define DBGRID_COL_PAD 6
#define DBGRID_PAD 3
#define DBGRID_SORT_W 10
#define DBGRID_MIN_ROWS 3
#define DBGRID_MAX_NAME 64
#define DBGRID_RESIZE_ZONE 3
// ============================================================
// Sort direction (matches ListView for consistency)
// ============================================================
#define SORT_NONE 0
#define SORT_ASC 1
#define SORT_DESC 2
// ============================================================
// Column definition
// ============================================================
typedef struct {
char fieldName[DBGRID_MAX_NAME];
char header[DBGRID_MAX_NAME];
int32_t width; // tagged size (0 = auto)
int32_t align; // 0=left, 1=center, 2=right
bool visible;
int32_t dataCol; // index into Data control's columns (-1 = unresolved)
} DbGridColT;
// ============================================================
// Per-instance data
// ============================================================
typedef struct {
WidgetT *dataWidget; // the Data control (set by form runtime)
// Column configuration
DbGridColT columns[DBGRID_MAX_COLS];
int32_t colCount;
// Display state
int32_t selectedRow; // data-row index (-1 = none)
int32_t scrollPos; // vertical scroll (row units)
int32_t scrollPosH; // horizontal scroll (pixel units)
int32_t resolvedW[DBGRID_MAX_COLS];
int32_t totalColW; // 0 = needs recalc
bool gridLines;
// Sort state
int32_t sortCol; // visible-column index (-1 = none)
int32_t sortDir; // SORT_NONE/ASC/DESC
int32_t *sortIndex; // display-row -> data-row mapping (NULL = natural)
int32_t sortCount; // length of sortIndex
// Column resize drag state
int32_t resizeCol;
int32_t resizeStartX;
int32_t resizeOrigW;
bool resizeDragging;
// Scrollbar drag state
int32_t sbDragOrient; // 0=vert, 1=horiz, -1=col resize
int32_t sbDragOff;
} DbGridDataT;
static int32_t sTypeId = -1;
// ============================================================
// Prototypes
// ============================================================
static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy);
static void dbGridBuildSortIndex(WidgetT *w);
static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font);
static void dbGridDestroy(WidgetT *w);
static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my);
static void dbGridOnDragEnd(WidgetT *w);
static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod);
static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
static int32_t getDataRowCount(const DbGridDataT *d);
static void resolveColumns(WidgetT *w, const BitmapFontT *font);
// ============================================================
// Helpers
// ============================================================
static int32_t getDataRowCount(const DbGridDataT *d) {
if (!d->dataWidget) {
return 0;
}
return wgtDataCtrlGetRowCount(d->dataWidget);
}
// Map a data-row to display-row (linear search in sortIndex)
static int32_t dataRowToDisplay(const DbGridDataT *d, int32_t dataRow) {
if (!d->sortIndex) {
return dataRow;
}
for (int32_t i = 0; i < d->sortCount; i++) {
if (d->sortIndex[i] == dataRow) {
return i;
}
}
return dataRow;
}
// Map a display-row to data-row
static int32_t displayToDataRow(const DbGridDataT *d, int32_t dispRow) {
if (!d->sortIndex || dispRow < 0 || dispRow >= d->sortCount) {
return dispRow;
}
return d->sortIndex[dispRow];
}
// Get cell text for a display-row and visible-column index
static const char *getCellText(const DbGridDataT *d, int32_t dispRow, int32_t visCol) {
if (!d->dataWidget || visCol < 0 || visCol >= d->colCount || !d->columns[visCol].visible) {
return "";
}
int32_t dataRow = displayToDataRow(d, dispRow);
int32_t dataCol = d->columns[visCol].dataCol;
if (dataCol < 0) {
return "";
}
return wgtDataCtrlGetCellText(d->dataWidget, dataRow, dataCol);
}
// ============================================================
// resolveColumns -- compute pixel widths for each column
// ============================================================
static void resolveColumns(WidgetT *w, const BitmapFontT *font) {
DbGridDataT *d = (DbGridDataT *)w->data;
if (d->totalColW > 0) {
return;
}
int32_t rowCount = getDataRowCount(d);
d->totalColW = 0;
for (int32_t c = 0; c < d->colCount; c++) {
if (!d->columns[c].visible) {
d->resolvedW[c] = 0;
continue;
}
if (d->columns[c].width > 0) {
d->resolvedW[c] = wgtResolveSize(d->columns[c].width, 0, font->charWidth);
} else {
// Auto-size: find widest content
int32_t maxW = textWidth(font, d->columns[c].header) + DBGRID_COL_PAD;
for (int32_t r = 0; r < rowCount; r++) {
int32_t dataCol = d->columns[c].dataCol;
if (dataCol >= 0) {
const char *text = wgtDataCtrlGetCellText(d->dataWidget, r, dataCol);
int32_t tw = textWidth(font, text) + DBGRID_COL_PAD;
if (tw > maxW) {
maxW = tw;
}
}
}
// Add space for sort indicator on header
maxW += DBGRID_SORT_W;
d->resolvedW[c] = maxW;
}
if (d->resolvedW[c] < DBGRID_MIN_COL_W) {
d->resolvedW[c] = DBGRID_MIN_COL_W;
}
d->totalColW += d->resolvedW[c];
}
}
// ============================================================
// dbGridBuildSortIndex -- sort rows by the current sort column
// ============================================================
static void dbGridBuildSortIndex(WidgetT *w) {
DbGridDataT *d = (DbGridDataT *)w->data;
free(d->sortIndex);
d->sortIndex = NULL;
d->sortCount = 0;
if (d->sortDir == SORT_NONE || d->sortCol < 0 || !d->dataWidget) {
return;
}
int32_t rowCount = getDataRowCount(d);
if (rowCount <= 0) {
return;
}
int32_t dataCol = d->columns[d->sortCol].dataCol;
if (dataCol < 0) {
return;
}
d->sortIndex = (int32_t *)malloc(rowCount * sizeof(int32_t));
d->sortCount = rowCount;
for (int32_t i = 0; i < rowCount; i++) {
d->sortIndex[i] = i;
}
// Insertion sort (stable, good for small N)
for (int32_t i = 1; i < rowCount; i++) {
int32_t key = d->sortIndex[i];
const char *keyStr = wgtDataCtrlGetCellText(d->dataWidget, key, dataCol);
int32_t j = i - 1;
while (j >= 0) {
int32_t cmpRow = d->sortIndex[j];
const char *cmpStr = wgtDataCtrlGetCellText(d->dataWidget, cmpRow, dataCol);
int32_t cmp = strcasecmp(keyStr, cmpStr);
if (d->sortDir == SORT_DESC) {
cmp = -cmp;
}
if (cmp >= 0) {
break;
}
d->sortIndex[j + 1] = d->sortIndex[j];
j--;
}
d->sortIndex[j + 1] = key;
}
}
// ============================================================
// Scrollbar geometry helpers (matches ListView pattern)
// ============================================================
static void computeLayout(WidgetT *w, const BitmapFontT *font, int32_t *outInnerW, int32_t *outInnerH, int32_t *outHeaderH, int32_t *outVisRows, bool *outNeedV, bool *outNeedH) {
DbGridDataT *d = (DbGridDataT *)w->data;
int32_t headerH = font->charHeight + 4;
int32_t innerW = w->w - DBGRID_BORDER * 2;
int32_t innerH = w->h - DBGRID_BORDER * 2 - headerH;
int32_t rowH = font->charHeight + 2;
int32_t rowCount = getDataRowCount(d);
int32_t visRows = innerH / rowH;
resolveColumns(w, font);
bool needV = (rowCount > visRows);
if (needV) {
innerW -= WGT_SB_W;
}
bool needH = (d->totalColW > innerW);
if (needH) {
innerH -= WGT_SB_W;
visRows = innerH / rowH;
if (!needV && rowCount > visRows) {
needV = true;
innerW -= WGT_SB_W;
}
}
if (visRows < 0) {
visRows = 0;
}
*outInnerW = innerW;
*outInnerH = innerH;
*outHeaderH = headerH;
*outVisRows = visRows;
*outNeedV = needV;
*outNeedH = needH;
}
// ============================================================
// colBorderHit -- check if mouse X is on a column border
// ============================================================
static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy) {
DbGridDataT *d = (DbGridDataT *)w->data;
(void)vy;
int32_t x = w->x + DBGRID_BORDER - d->scrollPosH;
for (int32_t c = 0; c < d->colCount; c++) {
if (!d->columns[c].visible) {
continue;
}
x += d->resolvedW[c];
if (vx >= x - DBGRID_RESIZE_ZONE && vx <= x + DBGRID_RESIZE_ZONE) {
return c;
}
}
return -1;
}
// ============================================================
// dbGridCalcMinSize
// ============================================================
static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font) {
w->calcMinW = font->charWidth * 20 + DBGRID_BORDER * 2;
w->calcMinH = (font->charHeight + 2) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2;
}
// ============================================================
// dbGridDestroy
// ============================================================
static void dbGridDestroy(WidgetT *w) {
DbGridDataT *d = (DbGridDataT *)w->data;
if (d) {
free(d->sortIndex);
free(d);
}
}
// ============================================================
// dbGridGetCursorShape
// ============================================================
static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my) {
AppContextT *ctx = wgtGetContext(w);
if (!ctx) {
return 0;
}
int32_t headerH = ctx->font.charHeight + 4;
int32_t headerTop = w->y + DBGRID_BORDER;
if (my >= headerTop && my < headerTop + headerH) {
resolveColumns(w, &ctx->font);
if (colBorderHit(w, mx, my) >= 0) {
return CURSOR_RESIZE_H;
}
}
return 0;
}
// ============================================================
// dbGridOnKey
// ============================================================
static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
DbGridDataT *d = (DbGridDataT *)w->data;
AppContextT *ctx = wgtGetContext(w);
if (!ctx) {
return;
}
int32_t rowCount = getDataRowCount(d);
if (rowCount <= 0) {
return;
}
int32_t innerW, innerH, headerH, visRows;
bool needV, needH;
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
int32_t dispRow = dataRowToDisplay(d, d->selectedRow);
int32_t newDisp = dispRow;
// Up arrow
if (key == (0x48 | 0x100)) {
newDisp = dispRow > 0 ? dispRow - 1 : 0;
}
// Down arrow
if (key == (0x50 | 0x100)) {
newDisp = dispRow < rowCount - 1 ? dispRow + 1 : rowCount - 1;
}
// Page Up
if (key == (0x49 | 0x100)) {
newDisp = dispRow - visRows;
if (newDisp < 0) {
newDisp = 0;
}
}
// Page Down
if (key == (0x51 | 0x100)) {
newDisp = dispRow + visRows;
if (newDisp >= rowCount) {
newDisp = rowCount - 1;
}
}
// Home
if (key == (0x47 | 0x100)) {
newDisp = 0;
}
// End
if (key == (0x4F | 0x100)) {
newDisp = rowCount - 1;
}
// Enter = activate (fire DblClick)
if (key == '\r' || key == '\n') {
if (w->onDblClick) {
w->onDblClick(w);
}
return;
}
if (newDisp != dispRow) {
int32_t newDataRow = displayToDataRow(d, newDisp);
d->selectedRow = newDataRow;
// Auto-scroll
if (newDisp < d->scrollPos) {
d->scrollPos = newDisp;
} else if (newDisp >= d->scrollPos + visRows) {
d->scrollPos = newDisp - visRows + 1;
}
// Sync Data control cursor
if (d->dataWidget) {
wgtDataCtrlSetCurrentRow(d->dataWidget, newDataRow);
}
if (w->onClick) {
w->onClick(w);
}
wgtInvalidatePaint(w);
}
}
// ============================================================
// dbGridOnMouse
// ============================================================
static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
DbGridDataT *d = (DbGridDataT *)w->data;
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
sFocusedWidget = w;
int32_t innerW, innerH, headerH, visRows;
bool needV, needH;
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
int32_t rowH = ctx->font.charHeight + 2;
int32_t headerTop = w->y + DBGRID_BORDER;
int32_t dataTop = headerTop + headerH;
int32_t rowCount = getDataRowCount(d);
// Vertical scrollbar click
if (needV && vx >= w->x + w->w - DBGRID_BORDER - WGT_SB_W) {
int32_t sbY = dataTop;
int32_t sbH = innerH;
int32_t maxScroll = rowCount - visRows;
if (maxScroll < 0) {
maxScroll = 0;
}
int32_t relY = vy - sbY;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(sbH, WGT_SB_W, maxScroll, d->scrollPos, &thumbPos, &thumbSize);
if (relY < WGT_SB_W) {
// Up arrow
if (d->scrollPos > 0) {
d->scrollPos--;
}
} else if (relY >= sbH - WGT_SB_W) {
// Down arrow
if (d->scrollPos < maxScroll) {
d->scrollPos++;
}
} else if (relY >= thumbPos && relY < thumbPos + thumbSize) {
// Thumb drag
d->sbDragOrient = 0;
d->sbDragOff = relY - thumbPos;
} else if (relY < thumbPos) {
d->scrollPos -= visRows;
if (d->scrollPos < 0) {
d->scrollPos = 0;
}
} else {
d->scrollPos += visRows;
if (d->scrollPos > maxScroll) {
d->scrollPos = maxScroll;
}
}
wgtInvalidatePaint(w);
return;
}
// Horizontal scrollbar click
if (needH && vy >= w->y + w->h - DBGRID_BORDER - WGT_SB_W) {
int32_t sbX = w->x + DBGRID_BORDER;
int32_t sbW = innerW;
int32_t maxScroll = d->totalColW - innerW;
if (maxScroll < 0) {
maxScroll = 0;
}
int32_t relX = vx - sbX;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(sbW, WGT_SB_W, maxScroll, d->scrollPosH, &thumbPos, &thumbSize);
if (relX < WGT_SB_W) {
d->scrollPosH -= 20;
if (d->scrollPosH < 0) {
d->scrollPosH = 0;
}
} else if (relX >= sbW - WGT_SB_W) {
d->scrollPosH += 20;
if (d->scrollPosH > maxScroll) {
d->scrollPosH = maxScroll;
}
} else if (relX >= thumbPos && relX < thumbPos + thumbSize) {
d->sbDragOrient = 1;
d->sbDragOff = relX - thumbPos;
}
wgtInvalidatePaint(w);
return;
}
// Column header area
if (vy >= headerTop && vy < dataTop) {
resolveColumns(w, &ctx->font);
// Check column border for resize
int32_t borderCol = colBorderHit(w, vx, vy);
if (borderCol >= 0) {
d->resizeCol = borderCol;
d->resizeStartX = vx;
d->resizeOrigW = d->resolvedW[borderCol];
d->resizeDragging = false;
d->sbDragOrient = -1;
return;
}
// Header click = sort
int32_t x = w->x + DBGRID_BORDER - d->scrollPosH;
for (int32_t c = 0; c < d->colCount; c++) {
if (!d->columns[c].visible) {
continue;
}
if (vx >= x && vx < x + d->resolvedW[c]) {
if (d->sortCol == c) {
d->sortDir = (d->sortDir == SORT_ASC) ? SORT_DESC : SORT_ASC;
} else {
d->sortCol = c;
d->sortDir = SORT_ASC;
}
dbGridBuildSortIndex(w);
wgtInvalidatePaint(w);
return;
}
x += d->resolvedW[c];
}
return;
}
// Data row click
if (vy >= dataTop && vy < dataTop + innerH) {
int32_t relY = vy - dataTop;
int32_t dispRow = d->scrollPos + relY / rowH;
if (dispRow >= 0 && dispRow < rowCount) {
int32_t dataRow = displayToDataRow(d, dispRow);
d->selectedRow = dataRow;
// Sync Data control cursor
if (d->dataWidget) {
wgtDataCtrlSetCurrentRow(d->dataWidget, dataRow);
}
if (w->onClick) {
w->onClick(w);
}
wgtInvalidatePaint(w);
}
}
}
// ============================================================
// dbGridOnDragUpdate
// ============================================================
static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
DbGridDataT *d = (DbGridDataT *)w->data;
(void)root;
(void)y;
// Column resize drag
if (d->sbDragOrient == -1) {
d->resizeDragging = true;
int32_t delta = x - d->resizeStartX;
int32_t newW = d->resizeOrigW + delta;
if (newW < DBGRID_MIN_COL_W) {
newW = DBGRID_MIN_COL_W;
}
d->totalColW += (newW - d->resolvedW[d->resizeCol]);
d->resolvedW[d->resizeCol] = newW;
wgtInvalidatePaint(w);
return;
}
// Vertical scrollbar thumb drag
if (d->sbDragOrient == 0) {
AppContextT *ctx = wgtGetContext(w);
if (!ctx) {
return;
}
int32_t innerW, innerH, headerH, visRows;
bool needV, needH;
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
int32_t rowCount = getDataRowCount(d);
int32_t maxScroll = rowCount - visRows;
if (maxScroll < 1) {
return;
}
int32_t sbY = w->y + DBGRID_BORDER + headerH;
int32_t sbH = innerH;
int32_t relY = y - sbY - d->sbDragOff;
int32_t track = sbH - WGT_SB_W * 2;
if (track > 0) {
d->scrollPos = (relY - WGT_SB_W) * maxScroll / track;
if (d->scrollPos < 0) {
d->scrollPos = 0;
}
if (d->scrollPos > maxScroll) {
d->scrollPos = maxScroll;
}
}
wgtInvalidatePaint(w);
return;
}
// Horizontal scrollbar thumb drag
if (d->sbDragOrient == 1) {
AppContextT *ctx = wgtGetContext(w);
if (!ctx) {
return;
}
int32_t innerW, innerH, headerH, visRows;
bool needV, needH;
computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
int32_t maxScroll = d->totalColW - innerW;
if (maxScroll < 1) {
return;
}
int32_t sbX = w->x + DBGRID_BORDER;
int32_t sbW = innerW;
int32_t relX = x - sbX - d->sbDragOff;
int32_t track = sbW - WGT_SB_W * 2;
if (track > 0) {
d->scrollPosH = (relX - WGT_SB_W) * maxScroll / track;
if (d->scrollPosH < 0) {
d->scrollPosH = 0;
}
if (d->scrollPosH > maxScroll) {
d->scrollPosH = maxScroll;
}
}
wgtInvalidatePaint(w);
}
}
// ============================================================
// dbGridOnDragEnd
// ============================================================
static void dbGridOnDragEnd(WidgetT *w) {
DbGridDataT *d = (DbGridDataT *)w->data;
d->sbDragOrient = -2;
d->resizeDragging = false;
}
// ============================================================
// dbGridPaint
// ============================================================
static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
DbGridDataT *d = (DbGridDataT *)w->data;
int32_t innerW, innerH, headerH, visRows;
bool needV, needH;
computeLayout(w, font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);
int32_t rowH = font->charHeight + 2;
int32_t rowCount = getDataRowCount(d);
// Outer sunken border
BevelStyleT bevel = { colors->windowShadow, colors->windowHighlight, colors->contentBg, DBGRID_BORDER };
drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);
int32_t contentX = w->x + DBGRID_BORDER;
int32_t contentY = w->y + DBGRID_BORDER;
// Save clip rect
int32_t oldClipX = disp->clipX;
int32_t oldClipY = disp->clipY;
int32_t oldClipW = disp->clipW;
int32_t oldClipH = disp->clipH;
// ---- Column Headers ----
setClipRect(disp, contentX, contentY, innerW, headerH);
int32_t hx = contentX - d->scrollPosH;
for (int32_t c = 0; c < d->colCount; c++) {
if (!d->columns[c].visible) {
continue;
}
int32_t cw = d->resolvedW[c];
if (hx + cw > contentX - 20 && hx < contentX + innerW + 20) {
BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
drawBevel(disp, ops, hx, contentY, cw, headerH, &hdrBevel);
// Header text (centered)
const char *hdr = d->columns[c].header;
int32_t tw = textWidth(font, hdr);
int32_t tx = hx + (cw - tw) / 2;
int32_t ty = contentY + (headerH - font->charHeight) / 2;
drawText(disp, ops, font, tx, ty, hdr, colors->contentFg, colors->buttonFace, false);
// Sort indicator
if (c == d->sortCol && d->sortDir != SORT_NONE) {
int32_t sx = hx + cw - DBGRID_SORT_W;
int32_t sy = contentY + headerH / 2;
if (d->sortDir == SORT_ASC) {
for (int32_t i = 0; i < 3; i++) {
drawHLine(disp, ops, sx + 3 - i, sy - 1 + i, 1 + i * 2, colors->contentFg);
}
} else {
for (int32_t i = 0; i < 3; i++) {
drawHLine(disp, ops, sx + 3 - i, sy + 1 - i, 1 + i * 2, colors->contentFg);
}
}
}
}
hx += cw;
}
// Fill remaining header space
if (hx < contentX + innerW) {
BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
drawBevel(disp, ops, hx, contentY, contentX + innerW - hx, headerH, &hdrBevel);
}
// ---- Data Rows ----
int32_t dataY = contentY + headerH;
setClipRect(disp, contentX, dataY, innerW, innerH);
// Background fill
rectFill(disp, ops, contentX, dataY, innerW, innerH, colors->contentBg);
for (int32_t i = 0; i < visRows && d->scrollPos + i < rowCount; i++) {
int32_t dispRow = d->scrollPos + i;
int32_t dataRow = displayToDataRow(d, dispRow);
int32_t ry = dataY + i * rowH;
// Alternating row background
if (i % 2 == 1) {
uint32_t altBg = colors->contentBg - 0x080808;
rectFill(disp, ops, contentX, ry, innerW, rowH, altBg);
}
// Selection highlight
bool selected = (dataRow == d->selectedRow);
if (selected) {
rectFill(disp, ops, contentX, ry, innerW, rowH, colors->menuHighlightBg);
}
uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg;
uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? colors->contentBg - 0x080808 : colors->contentBg);
// Draw cells
int32_t cx = contentX - d->scrollPosH;
for (int32_t c = 0; c < d->colCount; c++) {
if (!d->columns[c].visible) {
continue;
}
int32_t cw = d->resolvedW[c];
if (cx + cw > contentX && cx < contentX + innerW) {
const char *text = getCellText(d, dispRow, c);
int32_t tw = textWidth(font, text);
int32_t tx;
if (d->columns[c].align == 1) {
tx = cx + (cw - tw) / 2;
} else if (d->columns[c].align == 2) {
tx = cx + cw - tw - DBGRID_PAD;
} else {
tx = cx + DBGRID_PAD;
}
int32_t ty = ry + (rowH - font->charHeight) / 2;
drawText(disp, ops, font, tx, ty, text, fg, bg, false);
}
// Grid line
if (d->gridLines && cx + cw > contentX && cx + cw <= contentX + innerW) {
drawVLine(disp, ops, cx + cw - 1, ry, rowH, colors->windowShadow);
}
cx += cw;
}
// Horizontal grid line
if (d->gridLines) {
drawHLine(disp, ops, contentX, ry + rowH - 1, innerW, colors->windowShadow);
}
}
setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);
// ---- Scrollbars ----
if (needV) {
int32_t sbX = contentX + innerW;
int32_t sbY = dataY;
widgetDrawScrollbarV(disp, ops, colors, sbX, sbY, innerH, rowCount, visRows, d->scrollPos);
}
if (needH) {
int32_t sbX = contentX;
int32_t sbY = dataY + innerH;
widgetDrawScrollbarH(disp, ops, colors, sbX, sbY, innerW, d->totalColW, innerW, d->scrollPosH);
if (needV) {
rectFill(disp, ops, contentX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace);
}
}
// Focus rect
if (w == sFocusedWidget) {
drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg);
}
}
// ============================================================
// Public API
// ============================================================
static WidgetT *dbGridCreate(WidgetT *parent) {
if (!parent) {
return NULL;
}
WidgetT *w = widgetAlloc(parent, sTypeId);
if (w) {
DbGridDataT *d = (DbGridDataT *)calloc(1, sizeof(DbGridDataT));
if (d) {
d->selectedRow = -1;
d->sortCol = -1;
d->sbDragOrient = -2;
d->gridLines = true;
}
w->data = d;
}
return w;
}
// Set the Data control widget that this grid reads from.
// Also auto-populates columns from the Data control's column names.
void dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget) {
if (!w || !w->data) {
return;
}
DbGridDataT *d = (DbGridDataT *)w->data;
d->dataWidget = dataWidget;
d->totalColW = 0;
// Auto-populate columns
if (dataWidget) {
int32_t colCount = wgtDataCtrlGetColCount(dataWidget);
if (colCount > DBGRID_MAX_COLS) {
colCount = DBGRID_MAX_COLS;
}
d->colCount = colCount;
for (int32_t i = 0; i < colCount; i++) {
const char *name = wgtDataCtrlGetColName(dataWidget, i);
snprintf(d->columns[i].fieldName, DBGRID_MAX_NAME, "%s", name);
snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", name);
d->columns[i].width = 0;
d->columns[i].align = 0;
d->columns[i].visible = true;
d->columns[i].dataCol = i;
}
}
// Reset sort
free(d->sortIndex);
d->sortIndex = NULL;
d->sortCount = 0;
d->sortCol = -1;
d->sortDir = SORT_NONE;
wgtInvalidate(w);
}
// Refresh the grid display (e.g. after Data control refresh)
void dbGridRefresh(WidgetT *w) {
if (!w || !w->data) {
return;
}
DbGridDataT *d = (DbGridDataT *)w->data;
d->totalColW = 0;
// Rebuild sort index if sorting is active
if (d->sortDir != SORT_NONE) {
dbGridBuildSortIndex(w);
}
// Clamp scroll and selection
int32_t rowCount = getDataRowCount(d);
if (d->scrollPos >= rowCount) {
d->scrollPos = rowCount > 0 ? rowCount - 1 : 0;
}
if (d->selectedRow >= rowCount) {
d->selectedRow = rowCount > 0 ? rowCount - 1 : -1;
}
wgtInvalidate(w);
}
// Hide or show a column by field name
void dbGridSetColumnVisible(WidgetT *w, const char *fieldName, bool visible) {
if (!w || !w->data || !fieldName) {
return;
}
DbGridDataT *d = (DbGridDataT *)w->data;
for (int32_t i = 0; i < d->colCount; i++) {
if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
d->columns[i].visible = visible;
d->totalColW = 0;
wgtInvalidate(w);
return;
}
}
}
// Set a column's header text
void dbGridSetColumnHeader(WidgetT *w, const char *fieldName, const char *header) {
if (!w || !w->data || !fieldName || !header) {
return;
}
DbGridDataT *d = (DbGridDataT *)w->data;
for (int32_t i = 0; i < d->colCount; i++) {
if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", header);
d->totalColW = 0;
wgtInvalidate(w);
return;
}
}
}
// Set a column's width (tagged size, 0 = auto)
void dbGridSetColumnWidth(WidgetT *w, const char *fieldName, int32_t width) {
if (!w || !w->data || !fieldName) {
return;
}
DbGridDataT *d = (DbGridDataT *)w->data;
for (int32_t i = 0; i < d->colCount; i++) {
if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
d->columns[i].width = width;
d->totalColW = 0;
wgtInvalidate(w);
return;
}
}
}
// Get the selected data-row index (-1 = none)
int32_t dbGridGetSelectedRow(const WidgetT *w) {
if (!w || !w->data) {
return -1;
}
return ((DbGridDataT *)w->data)->selectedRow;
}
// ============================================================
// Property getters/setters
// ============================================================
static bool dbGridGetGridLines(const WidgetT *w) {
return ((DbGridDataT *)w->data)->gridLines;
}
static void dbGridSetGridLines(WidgetT *w, bool val) {
((DbGridDataT *)w->data)->gridLines = val;
wgtInvalidatePaint(w);
}
// ============================================================
// DXE registration
// ============================================================
static const WidgetClassT sClass = {
.version = WGT_CLASS_VERSION,
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
.handlers = {
[WGT_METHOD_PAINT] = (void *)dbGridPaint,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)dbGridCalcMinSize,
[WGT_METHOD_ON_MOUSE] = (void *)dbGridOnMouse,
[WGT_METHOD_ON_KEY] = (void *)dbGridOnKey,
[WGT_METHOD_DESTROY] = (void *)dbGridDestroy,
[WGT_METHOD_GET_CURSOR_SHAPE] = (void *)dbGridGetCursorShape,
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)dbGridOnDragUpdate,
[WGT_METHOD_ON_DRAG_END] = (void *)dbGridOnDragEnd,
}
};
static const struct {
WidgetT *(*create)(WidgetT *parent);
void (*setDataWidget)(WidgetT *w, WidgetT *dataWidget);
void (*refresh)(WidgetT *w);
void (*setColumnVisible)(WidgetT *w, const char *fieldName, bool visible);
void (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
void (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
int32_t (*getSelectedRow)(const WidgetT *w);
} sApi = {
.create = dbGridCreate,
.setDataWidget = dbGridSetDataWidget,
.refresh = dbGridRefresh,
.setColumnVisible = dbGridSetColumnVisible,
.setColumnHeader = dbGridSetColumnHeader,
.setColumnWidth = dbGridSetColumnWidth,
.getSelectedRow = dbGridGetSelectedRow,
};
static const WgtPropDescT sProps[] = {
{ "GridLines", WGT_IFACE_BOOL, (void *)dbGridGetGridLines, (void *)dbGridSetGridLines, NULL },
};
static const WgtMethodDescT sMethods[] = {
{ "Refresh", WGT_SIG_VOID, (void *)dbGridRefresh },
};
static const WgtEventDescT sEvents[] = {
{ "Click" },
{ "DblClick" },
};
static const WgtIfaceT sIface = {
.basName = "DBGrid",
.props = sProps,
.propCount = 1,
.methods = sMethods,
.methodCount = 1,
.events = sEvents,
.eventCount = 2,
.createSig = WGT_CREATE_PARENT,
.isContainer = false,
.defaultEvent = "DblClick",
.namePrefix = "DBGrid",
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClass);
wgtRegisterApi("dbgrid", &sApi);
wgtRegisterIface("dbgrid", &sIface);
}