#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 #include #include #include // ============================================================ // 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); }