#define DVX_WIDGET_IMPL // widgetListView.c -- ListView (multi-column list) widget // // A multi-column list with clickable/sortable column headers, horizontal // and vertical scrolling, column resize by dragging header borders, and // multi-select support. This is the most complex widget in the toolkit. // // Data model: cell data is stored as a flat array of const char* pointers // in row-major order (cellData[row * colCount + col]). This external data // model (pointers, not copies) keeps memory usage minimal and avoids // copying strings that the application already owns. // // ListView state is heap-allocated separately (ListViewDataT*) rather than // inlined in the widget union because the state is too large for a union // member -- it includes per-column resolved widths, sort index, selection // bits, and numerous scroll/drag state fields. // // Sort: clicking a column header toggles sort direction. Sorting uses an // indirection array (sortIndex) rather than rearranging the cellData. This // is critical because: // 1. The application owns cellData and may not expect it to be mutated // 2. Multi-select selBits are indexed by data row, not display row -- // rearranging data would invalidate all selection state // 3. The indirection allows O(1) display<->data row mapping // // Insertion sort is used because it's stable (preserves original order for // equal keys) and has good performance for the typical case of partially-sorted // data. For the row counts typical in a DOS GUI (hundreds, not millions), // insertion sort's O(n^2) worst case is acceptable and the constant factors // are lower than quicksort. // // Column widths support four sizing modes via a tagged integer: // - 0: auto-size (scan all data for widest string) // - positive small: character count (resolved via font->charWidth) // - positive large: pixel width // - negative: percentage of parent width // The resolution is done lazily in resolveColumnWidths, which is called on // first paint or mouse event after data changes. // // Column resize: dragging a column header border (3px hot zone) resizes the // column. The drag state (resizeCol, resizeStartX, resizeOrigW, resizeDragging) // is stored in the ListView's private data. The core only keeps sResizeListView // to route drag events. A minimum column width (LISTVIEW_MIN_COL_W) prevents // columns from collapsing to zero. // // Horizontal scrolling is needed when total column width exceeds the widget's // inner width. Both headers and data rows are offset by scrollPosH. The // horizontal scrollbar appears automatically when needed, and its presence // may trigger the vertical scrollbar to appear (and vice versa) -- this // two-pass logic handles the interdependency correctly. #include "dvxWgtP.h" #include "../listhelp/listHelp.h" #define LISTVIEW_BORDER 2 #define LISTVIEW_MIN_COL_W 20 static int32_t sTypeId = -1; #include #include #include "stb_ds_wrap.h" #define LISTVIEW_MAX_COLS 16 #define LISTVIEW_PAD 3 #define LISTVIEW_MIN_ROWS 4 #define LISTVIEW_COL_PAD 6 #define LISTVIEW_SORT_W 10 // ListView private data -- heap-allocated per widget, stored in w->data. // This replaces the old as.listView union pointer. The struct includes // column resize drag state that was previously in core globals. typedef struct { const ListViewColT *cols; int32_t colCount; const char **cellData; int32_t rowCount; int32_t selectedIdx; int32_t scrollPos; int32_t scrollPosH; int32_t sortCol; ListViewSortE sortDir; int32_t resolvedColW[LISTVIEW_MAX_COLS]; int32_t totalColW; int32_t *sortIndex; bool multiSelect; int32_t anchorIdx; uint8_t *selBits; bool reorderable; int32_t dragIdx; int32_t dropIdx; void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); // Column resize drag state (moved from core globals) int32_t resizeCol; int32_t resizeStartX; int32_t resizeOrigW; bool resizeDragging; int32_t sbDragOrient; int32_t sbDragOff; // Owned cell storage for AddItem/RemoveRow/Clear/SetCell char **ownedCells; // stb_ds dynamic array of strdup'd strings bool ownsCells; // true if cellData points to ownedCells int32_t nextCell; // next cell position for AddItem (row-major index) } ListViewDataT; // ============================================================ // Prototypes // ============================================================ static void allocListViewSelBits(WidgetT *w); static void listViewBuildSortIndex(WidgetT *w); static void listViewSyncOwned(WidgetT *w); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); void wgtListViewSelectAll(WidgetT *w); // ============================================================ // allocListViewSelBits // ============================================================ static void allocListViewSelBits(WidgetT *w) { ListViewDataT *lv = (ListViewDataT *)w->data; if (lv->selBits) { free(lv->selBits); lv->selBits = NULL; } int32_t count = lv->rowCount; if (count > 0 && lv->multiSelect) { lv->selBits = (uint8_t *)calloc(count, 1); } } // ============================================================ // listViewBuildSortIndex // ============================================================ // // Build or rebuild the sortIndex array. Uses insertion sort // (stable) on the sort column. If no sort is active, frees // the index so paint uses natural order. // Build the sort indirection array. Rather than rearranging cellData (which // the application owns and indexes into), we maintain a separate array that // maps display-row -> data-row. Paint and keyboard navigation work in display-row // space, then map through sortIndex to get the data row for cell lookups and // selection operations. This decoupling is essential because selBits are // indexed by data row -- sorting must not invalidate selection state. static void listViewBuildSortIndex(WidgetT *w) { ListViewDataT *lv = (ListViewDataT *)w->data; int32_t rowCount = lv->rowCount; int32_t sortCol = lv->sortCol; int32_t colCount = lv->colCount; // No sort active -- clear index if (sortCol < 0 || lv->sortDir == ListViewSortNoneE || rowCount <= 0) { if (lv->sortIndex) { free(lv->sortIndex); lv->sortIndex = NULL; } return; } // Allocate or reuse int32_t *idx = lv->sortIndex; if (!idx) { idx = (int32_t *)malloc(rowCount * sizeof(int32_t)); if (!idx) { return; } lv->sortIndex = idx; } // Initialize identity for (int32_t i = 0; i < rowCount; i++) { idx[i] = i; } // Insertion sort -- stable (equal elements keep their original order), // O(n^2) worst case but with very low constant factors. For typical // ListView row counts (10s to low 100s), this beats quicksort because // there's no recursion overhead, no stack usage, and the inner loop // is a simple pointer chase. On a 486, the predictable memory access // pattern also helps since insertion sort is cache-friendly (sequential // access to adjacent elements). bool ascending = (lv->sortDir == ListViewSortAscE); for (int32_t i = 1; i < rowCount; i++) { int32_t key = idx[i]; const char *keyStr = lv->cellData[key * colCount + sortCol]; if (!keyStr) { keyStr = ""; } int32_t j = i - 1; while (j >= 0) { const char *jStr = lv->cellData[idx[j] * colCount + sortCol]; if (!jStr) { jStr = ""; } int32_t cmp = strcmp(jStr, keyStr); if (!ascending) { cmp = -cmp; } if (cmp <= 0) { break; } idx[j + 1] = idx[j]; j--; } idx[j + 1] = key; } } // ============================================================ // listViewSyncOwned // ============================================================ // Sync cellData/rowCount from ownedCells after mutation. // Rebuilds sort index and selection bits, forces column width recalculation. static void listViewSyncOwned(WidgetT *w) { ListViewDataT *lv = (ListViewDataT *)w->data; int32_t cellCount = (int32_t)arrlen(lv->ownedCells); lv->cellData = (const char **)lv->ownedCells; lv->rowCount = (lv->colCount > 0) ? cellCount / lv->colCount : 0; lv->totalColW = 0; lv->ownsCells = true; listViewBuildSortIndex(w); allocListViewSelBits(w); wgtInvalidate(w); } // ============================================================ // resolveColumnWidths // ============================================================ // // Resolve tagged column sizes (pixels, chars, percent, or auto) // to actual pixel widths. Auto-sized columns (width==0) scan // cellData for the widest string in that column. // Resolve tagged column widths to actual pixel values. Column widths in the // ListViewColT definition can be specified as: // 0 = auto (scan data for widest string, include header) // positive = passed to wgtResolveSize (handles px, chars, or percent) // // Auto-sizing scans all rows for the widest string in that column, which // is O(rows * cols) but only runs once per data change (totalColW is reset // to 0 when data changes, triggering recalculation on next paint/mouse). static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) { ListViewDataT *lv = (ListViewDataT *)w->data; int32_t colCount = lv->colCount; int32_t parentW = w->w - LISTVIEW_BORDER * 2; if (parentW < 0) { parentW = 0; } int32_t totalW = 0; for (int32_t c = 0; c < colCount; c++) { int32_t taggedW = lv->cols[c].width; if (taggedW == 0) { // Auto-size: scan data for widest string in this column int32_t maxLen = (int32_t)strlen(lv->cols[c].title); for (int32_t r = 0; r < lv->rowCount; r++) { const char *cell = lv->cellData[r * colCount + c]; if (cell) { int32_t slen = (int32_t)strlen(cell); if (slen > maxLen) { maxLen = slen; } } } lv->resolvedColW[c] = maxLen * font->charWidth + LISTVIEW_COL_PAD; } else { lv->resolvedColW[c] = wgtResolveSize(taggedW, parentW, font->charWidth); } totalW += lv->resolvedColW[c]; } lv->totalColW = totalW; } // ============================================================ // widgetListViewColBorderHit // ============================================================ // // Returns true if (vx, vy) is on a column header border (resize zone). // Coordinates are in widget/virtual space (scroll-adjusted). // Test if a point is on a column header border (within 3px). Used by the // cursor-shape logic to show a resize cursor when hovering over column borders. // The 3px zone on each side of the border line makes it easy to grab even // on low-resolution displays where pixel-precise clicking is difficult. bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) { if (!w || w->type != sTypeId) { return false; } const ListViewDataT *lv = (const ListViewDataT *)w->data; if (lv->colCount == 0) { return false; } AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t headerH = font->charHeight + 4; int32_t headerTop = w->y + LISTVIEW_BORDER; if (vy < headerTop || vy >= headerTop + headerH) { return false; } int32_t colX = w->x + LISTVIEW_BORDER - lv->scrollPosH; for (int32_t c = 0; c < lv->colCount; c++) { int32_t border = colX + lv->resolvedColW[c]; if (vx >= border - 3 && vx <= border + 3) { return true; } colX += lv->resolvedColW[c]; } return false; } // ============================================================ // widgetListViewDestroy // ============================================================ // Free all heap-allocated ListView state. The sortIndex and selBits are // optional (may be NULL if no sort or single-select), so we check before // freeing. The ListViewDataT struct itself is always freed. void widgetListViewDestroy(WidgetT *w) { ListViewDataT *lv = (ListViewDataT *)w->data; for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) { free(lv->ownedCells[i]); } arrfree(lv->ownedCells); free(lv->selBits); free(lv->sortIndex); free(lv); w->data = NULL; } // ============================================================ // wgtListView // ============================================================ // ============================================================ // widgetListViewCalcMinSize // ============================================================ void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t headerH = font->charHeight + 4; int32_t minW = font->charWidth * 12 + LISTVIEW_BORDER * 2 + WGT_SB_W; w->calcMinW = minW; w->calcMinH = headerH + LISTVIEW_MIN_ROWS * font->charHeight + LISTVIEW_BORDER * 2; } // ============================================================ // widgetListViewOnKey // ============================================================ // Key handling must account for the sort indirection layer. Navigation happens // in display-row space (what the user sees on screen), but selection and data // access happen in data-row space (the original cellData indices). This means: // 1. Convert selectedIdx (data row) to display row via linear search in sortIndex // 2. Navigate in display space (widgetNavigateIndex) // 3. Convert back to data row via sortIndex[displayRow] // 4. Update selectedIdx and selBits using data-row indices // // Shift-select for sorted views is particularly tricky: the range must be // computed in display-row space (what the user sees as contiguous) but applied // to the data-row selBits. This means the selected data rows may not be // contiguous in the original data order. void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { ListViewDataT *lv = (ListViewDataT *)w->data; if (!w || w->type != sTypeId || lv->rowCount == 0) { return; } bool multi = lv->multiSelect; bool shift = (mod & KEY_MOD_SHIFT) != 0; bool ctrl = (mod & KEY_MOD_CTRL) != 0; int32_t rowCount = lv->rowCount; int32_t *sortIdx = lv->sortIndex; // Enter -- activate selected item (same as double-click) if (key == '\r' || key == '\n') { if (w->onDblClick && lv->selectedIdx >= 0) { w->onDblClick(w); } return; } // Ctrl+A -- select all (multi-select only) if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) { wgtListViewSelectAll(w); return; } // Space -- toggle current item (multi-select only) if (multi && key == ' ') { int32_t sel = lv->selectedIdx; if (sel >= 0 && lv->selBits) { lv->selBits[sel] ^= 1; lv->anchorIdx = sel; } if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); return; } // Find current display row from selectedIdx (data row) int32_t displaySel = -1; if (lv->selectedIdx >= 0) { if (sortIdx) { for (int32_t i = 0; i < rowCount; i++) { if (sortIdx[i] == lv->selectedIdx) { displaySel = i; break; } } } else { displaySel = lv->selectedIdx; } } // Compute visible rows for page up/down AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t headerH = font->charHeight + 4; int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t visibleRows = innerH / font->charHeight; if (visibleRows < 1) { visibleRows = 1; } int32_t newDisplaySel = widgetNavigateIndex(key, displaySel, rowCount, visibleRows); if (newDisplaySel < 0) { return; } displaySel = newDisplaySel; // Convert display row back to data row int32_t newDataRow = sortIdx ? sortIdx[displaySel] : displaySel; if (newDataRow == lv->selectedIdx) { return; } lv->selectedIdx = newDataRow; // Update multi-select if (multi && lv->selBits) { if (shift) { // Shift+arrow: range from anchor to new cursor (in data-row space) memset(lv->selBits, 0, rowCount); // Convert anchor to display row, then select display range mapped to data rows int32_t anchorDisplay = -1; int32_t anchor = lv->anchorIdx; if (sortIdx && anchor >= 0) { for (int32_t i = 0; i < rowCount; i++) { if (sortIdx[i] == anchor) { anchorDisplay = i; break; } } } else { anchorDisplay = anchor; } // Select all display rows in range, mapping to data rows int32_t lo = anchorDisplay < displaySel ? anchorDisplay : displaySel; int32_t hi = anchorDisplay > displaySel ? anchorDisplay : displaySel; if (lo < 0) { lo = 0; } if (hi >= rowCount) { hi = rowCount - 1; } for (int32_t i = lo; i <= hi; i++) { int32_t dr = sortIdx ? sortIdx[i] : i; lv->selBits[dr] = 1; } } // Plain arrow: just move cursor, leave selections untouched } // Scroll to keep selection visible (in display-row space) if (displaySel >= 0) { if (displaySel < lv->scrollPos) { lv->scrollPos = displaySel; } else if (displaySel >= lv->scrollPos + visibleRows) { lv->scrollPos = displaySel - visibleRows + 1; } } if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); } // ============================================================ // widgetListViewOnMouse // ============================================================ // Mouse handling is the most complex part of ListView. It handles four // distinct hit regions (checked in priority order): // 1. Vertical scrollbar (right edge, if visible) // 2. Horizontal scrollbar (bottom edge, if visible) // 3. Dead corner (when both scrollbars present -- no action) // 4. Column headers: resize drag (border +-3px) or sort toggle (click) // 5. Data rows: item selection with Ctrl/Shift modifiers, double-click, // and drag-reorder initiation // // The scrollbar visibility calculation is repeated here (matching paint) // because scrollbar presence depends on content/widget dimensions which // may have changed since the last paint. void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { sFocusedWidget = hit; ListViewDataT *lv = (ListViewDataT *)hit->data; AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; // Resolve column widths if needed if (lv->totalColW == 0 && lv->colCount > 0) { resolveColumnWidths(hit, font); } int32_t headerH = font->charHeight + 4; int32_t innerH = hit->h - LISTVIEW_BORDER * 2 - headerH; int32_t innerW = hit->w - LISTVIEW_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; int32_t totalColW = lv->totalColW; bool needVSb = (lv->rowCount > visibleRows); bool needHSb = false; // Scrollbar interdependency: adding a vertical scrollbar reduces innerW, // which may cause columns to overflow and require a horizontal scrollbar. // Adding a horizontal scrollbar reduces innerH, which may require a // vertical scrollbar. This two-pass check handles the mutual dependency. if (needVSb) { innerW -= WGT_SB_W; } if (totalColW > innerW) { needHSb = true; innerH -= WGT_SB_W; visibleRows = innerH / font->charHeight; if (!needVSb && lv->rowCount > visibleRows) { needVSb = true; innerW -= WGT_SB_W; } } if (visibleRows < 1) { visibleRows = 1; } // Clamp scroll positions int32_t maxScrollV = lv->rowCount - visibleRows; int32_t maxScrollH = totalColW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV); lv->scrollPosH = clampInt(lv->scrollPosH, 0, maxScrollH); // Check vertical scrollbar if (needVSb) { int32_t sbX = hit->x + hit->w - LISTVIEW_BORDER - WGT_SB_W; int32_t sbY = hit->y + LISTVIEW_BORDER + headerH; if (vx >= sbX && vy >= sbY && vy < sbY + innerH) { int32_t relY = vy - sbY; ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, lv->rowCount, visibleRows, lv->scrollPos); if (sh == ScrollHitArrowDecE) { if (lv->scrollPos > 0) { lv->scrollPos--; } } else if (sh == ScrollHitArrowIncE) { if (lv->scrollPos < maxScrollV) { lv->scrollPos++; } } else if (sh == ScrollHitPageDecE) { lv->scrollPos -= visibleRows; lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV); } else if (sh == ScrollHitPageIncE) { lv->scrollPos += visibleRows; lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV); } else if (sh == ScrollHitThumbE) { int32_t trackLen = innerH - WGT_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, lv->rowCount, visibleRows, lv->scrollPos, &thumbPos, &thumbSize); sDragWidget = hit; lv->sbDragOrient = 0; lv->sbDragOff = relY - WGT_SB_W - thumbPos; } return; } } // Check horizontal scrollbar if (needHSb) { int32_t sbX = hit->x + LISTVIEW_BORDER; int32_t sbY = hit->y + hit->h - LISTVIEW_BORDER - WGT_SB_W; if (vy >= sbY && vx >= sbX && vx < sbX + innerW) { int32_t relX = vx - sbX; int32_t pageSize = innerW - font->charWidth; if (pageSize < font->charWidth) { pageSize = font->charWidth; } ScrollHitE sh = widgetScrollbarHitTest(innerW, relX, totalColW, innerW, lv->scrollPosH); if (sh == ScrollHitArrowDecE) { lv->scrollPosH -= font->charWidth; } else if (sh == ScrollHitArrowIncE) { lv->scrollPosH += font->charWidth; } else if (sh == ScrollHitPageDecE) { lv->scrollPosH -= pageSize; } else if (sh == ScrollHitPageIncE) { lv->scrollPosH += pageSize; } else if (sh == ScrollHitThumbE) { int32_t trackLen = innerW - WGT_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalColW, innerW, lv->scrollPosH, &thumbPos, &thumbSize); sDragWidget = hit; lv->sbDragOrient = 1; lv->sbDragOff = relX - WGT_SB_W - thumbPos; } lv->scrollPosH = clampInt(lv->scrollPosH, 0, maxScrollH); return; } } // Check dead corner (both scrollbars present) if (needVSb && needHSb) { int32_t cornerX = hit->x + hit->w - LISTVIEW_BORDER - WGT_SB_W; int32_t cornerY = hit->y + hit->h - LISTVIEW_BORDER - WGT_SB_W; if (vx >= cornerX && vy >= cornerY) { return; } } // Check column header area int32_t headerTop = hit->y + LISTVIEW_BORDER; if (vy >= headerTop && vy < headerTop + headerH) { // Check for column border resize (3px zone on each side of border) int32_t colX = hit->x + LISTVIEW_BORDER - lv->scrollPosH; for (int32_t c = 0; c < lv->colCount; c++) { int32_t cw = lv->resolvedColW[c]; int32_t border = colX + cw; if (vx >= border - 3 && vx <= border + 3 && c < lv->colCount) { if (multiClickDetect(vx, vy) >= 2) { // Double-click on column border: auto-size to fit content int32_t maxLen = (int32_t)strlen(lv->cols[c].title); for (int32_t r = 0; r < lv->rowCount; r++) { const char *cell = lv->cellData[r * lv->colCount + c]; if (cell) { int32_t slen = (int32_t)strlen(cell); if (slen > maxLen) { maxLen = slen; } } } int32_t newW = maxLen * font->charWidth + LISTVIEW_COL_PAD; if (newW < LISTVIEW_MIN_COL_W) { newW = LISTVIEW_MIN_COL_W; } lv->resolvedColW[c] = newW; // Recalculate totalColW int32_t total = 0; for (int32_t tc = 0; tc < lv->colCount; tc++) { total += lv->resolvedColW[tc]; } lv->totalColW = total; wgtInvalidatePaint(hit); } else { // Start column resize drag (deferred until mouse moves) sDragWidget = hit; lv->sbDragOrient = -1; lv->resizeCol = c; lv->resizeStartX = vx; lv->resizeOrigW = cw; lv->resizeDragging = false; } return; } colX += cw; } // Not on a border -- check for sort click (disabled when reorderable // since sorting conflicts with manual ordering). Clicking a column // toggles between ascending/descending; clicking a different column // starts ascending. if (!lv->reorderable) { colX = hit->x + LISTVIEW_BORDER - lv->scrollPosH; for (int32_t c = 0; c < lv->colCount; c++) { int32_t cw = lv->resolvedColW[c]; if (vx >= colX && vx < colX + cw) { // Toggle sort direction for this column if (lv->sortCol == c) { if (lv->sortDir == ListViewSortAscE) { lv->sortDir = ListViewSortDescE; } else { lv->sortDir = ListViewSortAscE; } } else { lv->sortCol = c; lv->sortDir = ListViewSortAscE; } listViewBuildSortIndex(hit); if (lv->onHeaderClick) { lv->onHeaderClick(hit, c, lv->sortDir); } wgtInvalidatePaint(hit); return; } colX += cw; } } return; } // Click on data area int32_t dataTop = headerTop + headerH; int32_t relY = vy - dataTop; if (relY < 0) { return; } int32_t clickedRow = relY / font->charHeight; int32_t displayRow = lv->scrollPos + clickedRow; if (displayRow >= 0 && displayRow < lv->rowCount) { int32_t dataRow = lv->sortIndex ? lv->sortIndex[displayRow] : displayRow; lv->selectedIdx = dataRow; bool multi = lv->multiSelect; bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0; bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0; if (multi && lv->selBits) { if (ctrl) { // Ctrl+click: toggle item, update anchor lv->selBits[dataRow] ^= 1; lv->anchorIdx = dataRow; } else if (shift) { // Shift+click: range from anchor to clicked (in display-row space) memset(lv->selBits, 0, lv->rowCount); int32_t anchorDisplay = -1; int32_t anchor = lv->anchorIdx; if (lv->sortIndex && anchor >= 0) { for (int32_t i = 0; i < lv->rowCount; i++) { if (lv->sortIndex[i] == anchor) { anchorDisplay = i; break; } } } else { anchorDisplay = anchor; } int32_t lo = anchorDisplay < displayRow ? anchorDisplay : displayRow; int32_t hi = anchorDisplay > displayRow ? anchorDisplay : displayRow; if (lo < 0) { lo = 0; } if (hi >= lv->rowCount) { hi = lv->rowCount - 1; } for (int32_t i = lo; i <= hi; i++) { int32_t dr = lv->sortIndex ? lv->sortIndex[i] : i; lv->selBits[dr] = 1; } } else { // Plain click: select only this item, update anchor memset(lv->selBits, 0, lv->rowCount); lv->selBits[dataRow] = 1; lv->anchorIdx = dataRow; } } if (hit->onChange) { hit->onChange(hit); } // Double-click: fire onDblClick if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { hit->onDblClick(hit); } // Initiate drag-reorder if enabled (not from modifier clicks) if (lv->reorderable && !shift && !ctrl) { lv->dragIdx = dataRow; lv->dropIdx = dataRow; sDragWidget = hit; } } wgtInvalidatePaint(hit); } // ============================================================ // widgetListViewPaint // ============================================================ // Paint: the most involved paint function in the widget toolkit. Renders // in layers: // 1. Outer sunken bevel border // 2. Column headers (clipped to content width, scrolled by scrollPosH): // each header is a raised bevel button with centered text and optional // sort indicator (up/down triangle) // 3. Data rows (clipped to data area, scrolled both H and V): // background fill, then per-cell text rendering with alignment, then // selection highlight, then cursor focus rect in multi-select mode // 4. Drag-reorder insertion line (if active) // 5. Scrollbars (V and/or H, with dead corner fill) // 6. Outer focus rect // // Clip rectangles are saved/restored around the header and data sections to // prevent rendering from bleeding into the scrollbar or border areas. This // is cheaper than per-cell clipping and handles horizontal scroll correctly // (partially visible columns at the edges are clipped by the display's clip // rect rather than by the draw calls). void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { ListViewDataT *lv = (ListViewDataT *)w->data; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; // Resolve column widths if needed if (lv->totalColW == 0 && lv->colCount > 0) { resolveColumnWidths(w, font); } int32_t headerH = font->charHeight + 4; int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t innerW = w->w - LISTVIEW_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; int32_t totalColW = lv->totalColW; bool needVSb = (lv->rowCount > visibleRows); bool needHSb = false; if (needVSb) { innerW -= WGT_SB_W; } if (totalColW > innerW) { needHSb = true; innerH -= WGT_SB_W; visibleRows = innerH / font->charHeight; if (!needVSb && lv->rowCount > visibleRows) { needVSb = true; innerW -= WGT_SB_W; } } if (visibleRows < 1) { visibleRows = 1; } // Sunken border BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Clamp scroll positions int32_t maxScrollV = lv->rowCount - visibleRows; int32_t maxScrollH = totalColW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } lv->scrollPos = clampInt(lv->scrollPos, 0, maxScrollV); lv->scrollPosH = clampInt(lv->scrollPosH, 0, maxScrollH); int32_t baseX = w->x + LISTVIEW_BORDER; int32_t baseY = w->y + LISTVIEW_BORDER; int32_t colCount = lv->colCount; // ---- Draw column headers ---- { // Clip headers to the content area width int32_t oldClipX = d->clipX; int32_t oldClipY = d->clipY; int32_t oldClipW = d->clipW; int32_t oldClipH = d->clipH; setClipRect(d, baseX, baseY, innerW, headerH); int32_t hdrX = baseX - lv->scrollPosH; BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1); for (int32_t c = 0; c < colCount; c++) { int32_t cw = lv->resolvedColW[c]; // Draw raised button for header drawBevel(d, ops, hdrX, baseY, cw, headerH, &hdrBevel); // Header text int32_t textX = hdrX + LISTVIEW_PAD; int32_t textY = baseY + 2; int32_t availTextW = cw - LISTVIEW_PAD * 2; // Reserve space for sort indicator if this is the sort column if (c == lv->sortCol && lv->sortDir != ListViewSortNoneE) { availTextW -= LISTVIEW_SORT_W; } if (lv->cols[c].title) { int32_t titleLen = (int32_t)strlen(lv->cols[c].title); int32_t titleW = titleLen * font->charWidth; if (titleW > availTextW) { titleLen = availTextW / font->charWidth; } if (titleLen > 0) { drawTextN(d, ops, font, textX, textY, lv->cols[c].title, titleLen, colors->contentFg, colors->windowFace, true); } } // Sort indicator: a small filled triangle (4px tall) in the header. // Up triangle for ascending, down triangle for descending. Drawn as // horizontal lines of increasing/decreasing width, same technique as // the dropdown arrow glyph. if (c == lv->sortCol && lv->sortDir != ListViewSortNoneE) { int32_t cx = hdrX + cw - LISTVIEW_SORT_W / 2 - LISTVIEW_PAD; int32_t cy = baseY + headerH / 2; if (lv->sortDir == ListViewSortAscE) { // Up triangle (ascending) for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg); } } else { // Down triangle (descending) for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - 3 + i, cy - 1 + i, 7 - i * 2, colors->contentFg); } } } hdrX += cw; } // Fill any remaining header space to the right of columns if (hdrX < baseX + innerW) { drawBevel(d, ops, hdrX, baseY, baseX + innerW - hdrX, headerH, &hdrBevel); } setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); } // ---- Draw data rows ---- { int32_t dataY = baseY + headerH; // Set clip rect to data area int32_t oldClipX = d->clipX; int32_t oldClipY = d->clipY; int32_t oldClipW = d->clipW; int32_t oldClipH = d->clipH; setClipRect(d, baseX, dataY, innerW, innerH); int32_t scrollPos = lv->scrollPos; int32_t *sortIdx = lv->sortIndex; // Fill entire data area background first. This is done as a single // rectFill rather than per-row because it's cheaper to fill once and // then overdraw selected rows than to compute and fill each unselected // row's background separately. rectFill(d, ops, baseX, dataY, innerW, innerH, bg); bool multi = lv->multiSelect; uint8_t *selBits = lv->selBits; for (int32_t i = 0; i < visibleRows && (scrollPos + i) < lv->rowCount; i++) { int32_t displayRow = scrollPos + i; int32_t dataRow = sortIdx ? sortIdx[displayRow] : displayRow; int32_t iy = dataY + i * font->charHeight; uint32_t ifg = fg; uint32_t ibg = bg; bool selected = false; if (multi && selBits) { selected = selBits[dataRow] != 0; } else { selected = (dataRow == lv->selectedIdx); } if (selected) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; rectFill(d, ops, baseX, iy, innerW, font->charHeight, ibg); } // Draw each cell int32_t cellX = baseX - lv->scrollPosH; for (int32_t c = 0; c < colCount; c++) { int32_t cw = lv->resolvedColW[c]; const char *cell = lv->cellData[dataRow * colCount + c]; if (cell) { int32_t cellLen = (int32_t)strlen(cell); int32_t maxChars = (cw - LISTVIEW_PAD * 2) / font->charWidth; if (maxChars < 0) { maxChars = 0; } if (cellLen > maxChars) { cellLen = maxChars; } int32_t tx = cellX + LISTVIEW_PAD; // Column alignment: left (default), right, or center. // Right/center alignment compute the available width and // offset the text start position accordingly. This is done // per-cell rather than using a general-purpose aligned draw // function to keep the inner loop simple and avoid extra // function call overhead per cell. if (lv->cols[c].align == ListViewAlignRightE) { int32_t renderedW = cellLen * font->charWidth; int32_t availW = cw - LISTVIEW_PAD * 2; tx = cellX + LISTVIEW_PAD + (availW - renderedW); } else if (lv->cols[c].align == ListViewAlignCenterE) { int32_t renderedW = cellLen * font->charWidth; int32_t availW = cw - LISTVIEW_PAD * 2; tx = cellX + LISTVIEW_PAD + (availW - renderedW) / 2; } if (cellLen > 0) { drawTextN(d, ops, font, tx, iy, cell, cellLen, ifg, ibg, true); } } cellX += cw; } // Draw cursor focus rect in multi-select mode (on top of text) if (multi && dataRow == lv->selectedIdx && w == sFocusedWidget) { drawFocusRect(d, ops, baseX, iy, innerW, font->charHeight, fg); } } // Draw drag-reorder insertion indicator if (lv->reorderable && lv->dragIdx >= 0 && lv->dropIdx >= 0) { int32_t drop = lv->dropIdx; int32_t lineY = dataY + (drop - lv->scrollPos) * font->charHeight; if (lineY >= dataY && lineY <= dataY + innerH) { drawHLine(d, ops, baseX, lineY, innerW, fg); drawHLine(d, ops, baseX, lineY + 1, innerW, fg); } } setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); } // ---- Draw scrollbars ---- if (needVSb) { int32_t sbX = w->x + w->w - LISTVIEW_BORDER - WGT_SB_W; int32_t sbY = w->y + LISTVIEW_BORDER + headerH; widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, lv->rowCount, visibleRows, lv->scrollPos); } if (needHSb) { int32_t sbX = w->x + LISTVIEW_BORDER; int32_t sbY = w->y + w->h - LISTVIEW_BORDER - WGT_SB_W; widgetDrawScrollbarH(d, ops, colors, sbX, sbY, innerW, totalColW, innerW, lv->scrollPosH); // Fill the dead corner when both scrollbars are present. This is the // small square at the intersection of the two scrollbars (bottom-right) // that doesn't belong to either. Without filling it, stale content // from previous paints would show through. if (needVSb) { rectFill(d, ops, sbX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace); } } if (w == sFocusedWidget) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } } // ============================================================ // Public accessors (kept here due to static helper dependencies) // ============================================================ void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; if (count > LISTVIEW_MAX_COLS) { count = LISTVIEW_MAX_COLS; } lv->cols = cols; lv->colCount = count; lv->totalColW = 0; if (lv->rowCount > 0) { AppContextT *ctx = wgtGetContext(w); if (ctx) { resolveColumnWidths(w, &ctx->font); } } wgtInvalidate(w); } void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; if (lv->sortIndex) { free(lv->sortIndex); lv->sortIndex = NULL; } lv->cellData = cellData; lv->rowCount = rowCount; lv->totalColW = 0; if (lv->selectedIdx >= rowCount) { lv->selectedIdx = rowCount > 0 ? 0 : -1; } if (lv->selectedIdx < 0 && rowCount > 0) { lv->selectedIdx = 0; } lv->anchorIdx = lv->selectedIdx; listViewBuildSortIndex(w); allocListViewSelBits(w); if (lv->selBits && lv->selectedIdx >= 0) { lv->selBits[lv->selectedIdx] = 1; } if (lv->colCount > 0) { AppContextT *ctx = wgtGetContext(w); if (ctx) { resolveColumnWidths(w, &ctx->font); } } wgtInvalidate(w); } void wgtListViewSetMultiSelect(WidgetT *w, bool multi) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; lv->multiSelect = multi; allocListViewSelBits(w); if (lv->selBits && lv->selectedIdx >= 0) { lv->selBits[lv->selectedIdx] = 1; } } void wgtListViewSetReorderable(WidgetT *w, bool reorderable) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; lv->reorderable = reorderable; if (reorderable) { lv->sortCol = -1; lv->sortDir = ListViewSortNoneE; if (lv->sortIndex) { free(lv->sortIndex); lv->sortIndex = NULL; } } } void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; lv->sortCol = col; lv->sortDir = dir; listViewBuildSortIndex(w); wgtInvalidatePaint(w); } // ============================================================ // widgetListViewScrollDragUpdate // ============================================================ // Handle column resize drag. orient == -1 means column resize (sent by // widgetEvent.c when sResizeListView is set). The drag is deferred: we // don't start modifying column widths until the mouse actually moves from // the initial click position. This prevents accidental resizes from stray // clicks on column borders. static void widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { ListViewDataT *lv = (ListViewDataT *)w->data; // orient == -1 means column resize drag if (orient == -1) { if (!lv->resizeDragging) { if (mouseX == lv->resizeStartX) { return; } lv->resizeDragging = true; } int32_t delta = mouseX - lv->resizeStartX; int32_t newW = lv->resizeOrigW + delta; if (newW < LISTVIEW_MIN_COL_W) { newW = LISTVIEW_MIN_COL_W; } if (newW != lv->resolvedColW[lv->resizeCol]) { lv->resolvedColW[lv->resizeCol] = newW; int32_t total = 0; for (int32_t c = 0; c < lv->colCount; c++) { total += lv->resolvedColW[c]; } lv->totalColW = total; } return; } AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t headerH = font->charHeight + 4; int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t innerW = w->w - LISTVIEW_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; int32_t totalColW = lv->totalColW; bool needVSb = (lv->rowCount > visibleRows); if (needVSb) { innerW -= WGT_SB_W; } if (totalColW > innerW) { innerH -= WGT_SB_W; visibleRows = innerH / font->charHeight; if (!needVSb && lv->rowCount > visibleRows) { needVSb = true; innerW -= WGT_SB_W; } } if (visibleRows < 1) { visibleRows = 1; } if (orient == 0) { // Vertical scrollbar drag int32_t maxScroll = lv->rowCount - visibleRows; if (maxScroll <= 0) { return; } int32_t trackLen = innerH - WGT_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, lv->rowCount, visibleRows, lv->scrollPos, &thumbPos, &thumbSize); int32_t sbY = w->y + LISTVIEW_BORDER + headerH; int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; lv->scrollPos = clampInt(newScroll, 0, maxScroll); } else if (orient == 1) { // Horizontal scrollbar drag int32_t maxScroll = totalColW - innerW; if (maxScroll <= 0) { return; } int32_t trackLen = innerW - WGT_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalColW, innerW, lv->scrollPosH, &thumbPos, &thumbSize); int32_t sbX = w->x + LISTVIEW_BORDER; int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; lv->scrollPosH = clampInt(newScroll, 0, maxScroll); } } // ============================================================ // DXE registration // ============================================================ // ============================================================ // widgetListViewGetCursorShape // ============================================================ int32_t widgetListViewGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) { if (widgetListViewColBorderHit(w, vx, vy)) { return CURSOR_RESIZE_H; } return 0; } static void widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { (void)root; (void)x; ListViewDataT *lv = (ListViewDataT *)w->data; AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t headerH = font->charHeight + 4; int32_t dataY = w->y + LISTVIEW_BORDER + headerH; int32_t relY = y - dataY; int32_t dropIdx = lv->scrollPos + relY / font->charHeight; if (dropIdx < 0) { dropIdx = 0; } if (dropIdx > lv->rowCount) { dropIdx = lv->rowCount; } lv->dropIdx = dropIdx; // Auto-scroll near edges int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; int32_t visibleRows = innerH / font->charHeight; if (relY < font->charHeight && lv->scrollPos > 0) { lv->scrollPos--; } else if (relY >= (visibleRows - 1) * font->charHeight && lv->scrollPos < lv->rowCount - visibleRows) { lv->scrollPos++; } } static void widgetListViewReorderDrop(WidgetT *w) { ListViewDataT *lv = (ListViewDataT *)w->data; int32_t from = lv->dragIdx; int32_t to = lv->dropIdx; lv->dragIdx = -1; lv->dropIdx = -1; if (from < 0 || to < 0 || from == to || from == to - 1) { return; } if (from < 0 || from >= lv->rowCount) { return; } // Move row data by shifting the cellData pointers. // cellData is row-major: cellData[row * colCount + col]. int32_t cols = lv->colCount; const char **moving = (const char **)malloc(cols * sizeof(const char *)); if (!moving) { return; } // Save the moving row for (int32_t c = 0; c < cols; c++) { moving[c] = lv->cellData[from * cols + c]; } // Save the moving row's selection bit uint8_t movingSel = (lv->selBits && from < lv->rowCount) ? lv->selBits[from] : 0; if (to > from) { // Moving down: shift rows up for (int32_t r = from; r < to - 1; r++) { for (int32_t c = 0; c < cols; c++) { ((const char **)lv->cellData)[r * cols + c] = lv->cellData[(r + 1) * cols + c]; } if (lv->selBits) { lv->selBits[r] = lv->selBits[r + 1]; } } for (int32_t c = 0; c < cols; c++) { ((const char **)lv->cellData)[(to - 1) * cols + c] = moving[c]; } if (lv->selBits) { lv->selBits[to - 1] = movingSel; } lv->selectedIdx = to - 1; } else { // Moving up: shift rows down for (int32_t r = from; r > to; r--) { for (int32_t c = 0; c < cols; c++) { ((const char **)lv->cellData)[r * cols + c] = lv->cellData[(r - 1) * cols + c]; } if (lv->selBits) { lv->selBits[r] = lv->selBits[r - 1]; } } for (int32_t c = 0; c < cols; c++) { ((const char **)lv->cellData)[to * cols + c] = moving[c]; } if (lv->selBits) { lv->selBits[to] = movingSel; } lv->selectedIdx = to; } free(moving); if (w->onChange) { w->onChange(w); } } // ============================================================ // widgetListViewOnDragEnd // ============================================================ static void widgetListViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { (void)root; (void)x; (void)y; ListViewDataT *lv = (ListViewDataT *)w->data; if (lv->dragIdx >= 0) { widgetListViewReorderDrop(w); lv->dragIdx = -1; } lv->resizeDragging = false; wgtInvalidatePaint(w); } // ============================================================ // widgetListViewOnDragUpdate // ============================================================ static void widgetListViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { ListViewDataT *lv = (ListViewDataT *)w->data; if (lv->resizeDragging) { widgetListViewScrollDragUpdate(w, -1, 0, x, y); } else if (lv->dragIdx >= 0) { widgetListViewReorderUpdate(w, root, x, y); } else { widgetListViewScrollDragUpdate(w, lv->sbDragOrient, lv->sbDragOff, x, y); } } static const WidgetClassT sClassListView = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE, .handlers = { [WGT_METHOD_PAINT] = (void *)widgetListViewPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetListViewCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetListViewOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetListViewOnKey, [WGT_METHOD_DESTROY] = (void *)widgetListViewDestroy, [WGT_METHOD_GET_CURSOR_SHAPE] = (void *)widgetListViewGetCursorShape, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetListViewOnDragUpdate, [WGT_METHOD_ON_DRAG_END] = (void *)widgetListViewOnDragEnd, } }; // ============================================================ // Widget creation functions and accessors // ============================================================ WidgetT *wgtListView(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, sTypeId); if (!w) { return NULL; } ListViewDataT *lv = (ListViewDataT *)calloc(1, sizeof(ListViewDataT)); if (!lv) { free(w); return NULL; } lv->selectedIdx = -1; lv->anchorIdx = -1; lv->sortCol = -1; lv->sortDir = ListViewSortNoneE; lv->dragIdx = -1; lv->dropIdx = -1; lv->resizeCol = -1; w->data = lv; w->weight = 100; return w; } void wgtListViewAddItem(WidgetT *w, const char *text) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; if (lv->colCount <= 0) { return; } // If at a row boundary, add colCount empty cells to start a new row if (lv->nextCell % lv->colCount == 0) { for (int32_t c = 0; c < lv->colCount; c++) { arrput(lv->ownedCells, strdup("")); } } // Replace the next empty cell with the provided text int32_t idx = lv->nextCell; free(lv->ownedCells[idx]); lv->ownedCells[idx] = strdup(text ? text : ""); lv->nextCell++; listViewSyncOwned(w); } void wgtListViewClear(WidgetT *w) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; for (int32_t i = 0; i < (int32_t)arrlen(lv->ownedCells); i++) { free(lv->ownedCells[i]); } arrsetlen(lv->ownedCells, 0); lv->nextCell = 0; lv->selectedIdx = -1; lv->scrollPos = 0; lv->scrollPosH = 0; listViewSyncOwned(w); } void wgtListViewClearSelection(WidgetT *w) { if (!w || w->type != sTypeId) { return; } ListViewDataT *lv = (ListViewDataT *)w->data; if (!lv->selBits) { return; } memset(lv->selBits, 0, lv->rowCount); wgtInvalidatePaint(w); } const char *wgtListViewGetCell(const WidgetT *w, int32_t row, int32_t col) { if (!w || w->type != sTypeId) { return ""; } const ListViewDataT *lv = (const ListViewDataT *)w->data; if (row < 0 || row >= lv->rowCount || col < 0 || col >= lv->colCount) { return ""; } const char *cell = lv->cellData[row * lv->colCount + col]; return cell ? cell : ""; } int32_t wgtListViewGetRowCount(const WidgetT *w) { if (!w || w->type != sTypeId) { return 0; } const ListViewDataT *lv = (const ListViewDataT *)w->data; return lv->rowCount; } int32_t wgtListViewGetSelected(const WidgetT *w) { VALIDATE_WIDGET(w, sTypeId, -1); const ListViewDataT *lv = (const ListViewDataT *)w->data; return lv->selectedIdx; } bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) { VALIDATE_WIDGET(w, sTypeId, false); const ListViewDataT *lv = (const ListViewDataT *)w->data; if (!lv->multiSelect) { return idx == lv->selectedIdx; } if (!lv->selBits || idx < 0 || idx >= lv->rowCount) { return false; } return lv->selBits[idx] != 0; } void wgtListViewRemoveRow(WidgetT *w, int32_t row) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; if (row < 0 || row >= lv->rowCount || lv->colCount <= 0) { return; } // Remove colCount cells starting at row * colCount int32_t base = row * lv->colCount; for (int32_t c = 0; c < lv->colCount; c++) { free(lv->ownedCells[base + c]); } // Shift remaining cells down by colCount positions int32_t totalCells = (int32_t)arrlen(lv->ownedCells); for (int32_t i = base; i < totalCells - lv->colCount; i++) { lv->ownedCells[i] = lv->ownedCells[i + lv->colCount]; } arrsetlen(lv->ownedCells, totalCells - lv->colCount); // Adjust nextCell if it was past the removed row if (lv->nextCell > base + lv->colCount) { lv->nextCell -= lv->colCount; } else if (lv->nextCell > base) { lv->nextCell = base; } listViewSyncOwned(w); if (lv->selectedIdx >= lv->rowCount) { lv->selectedIdx = lv->rowCount > 0 ? lv->rowCount - 1 : -1; } } void wgtListViewSelectAll(WidgetT *w) { if (!w || w->type != sTypeId) { return; } ListViewDataT *lv = (ListViewDataT *)w->data; if (!lv->selBits) { return; } memset(lv->selBits, 1, lv->rowCount); wgtInvalidatePaint(w); } void wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; if (row < 0 || row >= lv->rowCount || col < 0 || col >= lv->colCount) { return; } if (!lv->ownsCells) { return; } int32_t idx = row * lv->colCount + col; free(lv->ownedCells[idx]); lv->ownedCells[idx] = strdup(text ? text : ""); // cellData pointer may have been invalidated by prior arrput; refresh it lv->cellData = (const char **)lv->ownedCells; lv->totalColW = 0; wgtInvalidate(w); } void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; lv->onHeaderClick = cb; } void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; if (!lv->selBits || idx < 0 || idx >= lv->rowCount) { return; } lv->selBits[idx] = selected ? 1 : 0; wgtInvalidatePaint(w); } void wgtListViewSetSelected(WidgetT *w, int32_t idx) { VALIDATE_WIDGET_VOID(w, sTypeId); ListViewDataT *lv = (ListViewDataT *)w->data; lv->selectedIdx = idx; lv->anchorIdx = idx; if (lv->selBits) { memset(lv->selBits, 0, lv->rowCount); if (idx >= 0 && idx < lv->rowCount) { lv->selBits[idx] = 1; } } wgtInvalidatePaint(w); } // ============================================================ // DXE registration // ============================================================ static const struct { WidgetT *(*create)(WidgetT *parent); void (*setColumns)(WidgetT *w, const ListViewColT *cols, int32_t count); void (*setData)(WidgetT *w, const char **cellData, int32_t rowCount); int32_t (*getSelected)(const WidgetT *w); void (*setSelected)(WidgetT *w, int32_t idx); void (*setSort)(WidgetT *w, int32_t col, ListViewSortE dir); void (*setHeaderClickCallback)(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)); void (*setMultiSelect)(WidgetT *w, bool multi); bool (*isItemSelected)(const WidgetT *w, int32_t idx); void (*setItemSelected)(WidgetT *w, int32_t idx, bool selected); void (*selectAll)(WidgetT *w); void (*clearSelection)(WidgetT *w); void (*setReorderable)(WidgetT *w, bool reorderable); void (*addItem)(WidgetT *w, const char *text); void (*removeRow)(WidgetT *w, int32_t row); void (*clear)(WidgetT *w); const char *(*getCell)(const WidgetT *w, int32_t row, int32_t col); void (*setCell)(WidgetT *w, int32_t row, int32_t col, const char *text); int32_t (*getRowCount)(const WidgetT *w); } sApi = { .create = wgtListView, .setColumns = wgtListViewSetColumns, .setData = wgtListViewSetData, .getSelected = wgtListViewGetSelected, .setSelected = wgtListViewSetSelected, .setSort = wgtListViewSetSort, .setHeaderClickCallback = wgtListViewSetHeaderClickCallback, .setMultiSelect = wgtListViewSetMultiSelect, .isItemSelected = wgtListViewIsItemSelected, .setItemSelected = wgtListViewSetItemSelected, .selectAll = wgtListViewSelectAll, .clearSelection = wgtListViewClearSelection, .setReorderable = wgtListViewSetReorderable, .addItem = wgtListViewAddItem, .removeRow = wgtListViewRemoveRow, .clear = wgtListViewClear, .getCell = wgtListViewGetCell, .setCell = wgtListViewSetCell, .getRowCount = wgtListViewGetRowCount }; static const WgtPropDescT sProps[] = { { "ListIndex", WGT_IFACE_INT, (void *)wgtListViewGetSelected, (void *)wgtListViewSetSelected, NULL } }; static const WgtMethodDescT sMethods[] = { { "AddItem", WGT_SIG_STR, (void *)wgtListViewAddItem }, { "Clear", WGT_SIG_VOID, (void *)wgtListViewClear }, { "ClearSelection", WGT_SIG_VOID, (void *)wgtListViewClearSelection }, { "GetCell", WGT_SIG_RET_STR_INT_INT, (void *)wgtListViewGetCell }, { "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListViewIsItemSelected }, { "RemoveItem", WGT_SIG_INT, (void *)wgtListViewRemoveRow }, { "RowCount", WGT_SIG_RET_INT, (void *)wgtListViewGetRowCount }, { "SelectAll", WGT_SIG_VOID, (void *)wgtListViewSelectAll }, { "SetCell", WGT_SIG_INT_INT_STR, (void *)wgtListViewSetCell }, { "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListViewSetItemSelected }, { "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListViewSetMultiSelect }, { "SetReorderable", WGT_SIG_BOOL, (void *)wgtListViewSetReorderable }, }; static const WgtIfaceT sIface = { .basName = "ListView", .props = sProps, .propCount = 1, .methods = sMethods, .methodCount = 12, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, .defaultEvent = "Click" }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassListView); wgtRegisterApi("listview", &sApi); wgtRegisterIface("listview", &sIface); }