// widgetListView.c — ListView (multi-column list) widget #include "widgetInternal.h" #include #include #define LISTVIEW_BORDER 2 #define LISTVIEW_PAD 3 #define LISTVIEW_SB_W 14 #define LISTVIEW_MIN_ROWS 4 #define LISTVIEW_COL_PAD 6 #define LISTVIEW_SORT_W 10 // ============================================================ // Prototypes // ============================================================ static void drawListViewHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW); static void drawListViewVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalRows, int32_t visibleRows); static void listViewBuildSortIndex(WidgetT *w); static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font); // ============================================================ // drawListViewHScrollbar // ============================================================ static void drawListViewHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW) { if (sbW < LISTVIEW_SB_W * 3) { return; } // Trough background BevelStyleT troughBevel = BEVEL_TROUGH(colors); drawBevel(d, ops, sbX, sbY, sbW, LISTVIEW_SB_W, &troughBevel); // Left arrow button BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); drawBevel(d, ops, sbX, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); // Left arrow triangle { int32_t cx = sbX + LISTVIEW_SB_W / 2; int32_t cy = sbY + LISTVIEW_SB_W / 2; uint32_t fg = colors->contentFg; for (int32_t i = 0; i < 4; i++) { drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); } } // Right arrow button int32_t rightX = sbX + sbW - LISTVIEW_SB_W; drawBevel(d, ops, rightX, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); // Right arrow triangle { int32_t cx = rightX + LISTVIEW_SB_W / 2; int32_t cy = sbY + LISTVIEW_SB_W / 2; uint32_t fg = colors->contentFg; for (int32_t i = 0; i < 4; i++) { drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); } } // Thumb int32_t trackLen = sbW - LISTVIEW_SB_W * 2; if (trackLen > 0 && totalW > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalW, visibleW, w->as.listView.scrollPosH, &thumbPos, &thumbSize); drawBevel(d, ops, sbX + LISTVIEW_SB_W + thumbPos, sbY, thumbSize, LISTVIEW_SB_W, &btnBevel); } } // ============================================================ // drawListViewVScrollbar // ============================================================ static void drawListViewVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalRows, int32_t visibleRows) { if (sbH < LISTVIEW_SB_W * 3) { return; } // Trough background BevelStyleT troughBevel = BEVEL_TROUGH(colors); drawBevel(d, ops, sbX, sbY, LISTVIEW_SB_W, sbH, &troughBevel); // Up arrow button BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); drawBevel(d, ops, sbX, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); // Up arrow triangle { int32_t cx = sbX + LISTVIEW_SB_W / 2; int32_t cy = sbY + LISTVIEW_SB_W / 2; uint32_t fg = colors->contentFg; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); } } // Down arrow button int32_t downY = sbY + sbH - LISTVIEW_SB_W; drawBevel(d, ops, sbX, downY, LISTVIEW_SB_W, LISTVIEW_SB_W, &btnBevel); // Down arrow triangle { int32_t cx = sbX + LISTVIEW_SB_W / 2; int32_t cy = downY + LISTVIEW_SB_W / 2; uint32_t fg = colors->contentFg; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); } } // Thumb int32_t trackLen = sbH - LISTVIEW_SB_W * 2; if (trackLen > 0 && totalRows > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalRows, visibleRows, w->as.listView.scrollPos, &thumbPos, &thumbSize); drawBevel(d, ops, sbX, sbY + LISTVIEW_SB_W + thumbPos, LISTVIEW_SB_W, thumbSize, &btnBevel); } } // ============================================================ // 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. static void listViewBuildSortIndex(WidgetT *w) { int32_t rowCount = w->as.listView.rowCount; int32_t sortCol = w->as.listView.sortCol; int32_t colCount = w->as.listView.colCount; // No sort active — clear index if (sortCol < 0 || w->as.listView.sortDir == ListViewSortNoneE || rowCount <= 0) { if (w->as.listView.sortIndex) { free(w->as.listView.sortIndex); w->as.listView.sortIndex = NULL; } return; } // Allocate or reuse int32_t *idx = w->as.listView.sortIndex; if (!idx) { idx = (int32_t *)malloc(rowCount * sizeof(int32_t)); if (!idx) { return; } w->as.listView.sortIndex = idx; } // Initialize identity for (int32_t i = 0; i < rowCount; i++) { idx[i] = i; } // Insertion sort — stable, O(n^2) but fine for typical row counts bool ascending = (w->as.listView.sortDir == ListViewSortAscE); for (int32_t i = 1; i < rowCount; i++) { int32_t key = idx[i]; const char *keyStr = w->as.listView.cellData[key * colCount + sortCol]; if (!keyStr) { keyStr = ""; } int32_t j = i - 1; while (j >= 0) { const char *jStr = w->as.listView.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; } } // ============================================================ // 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. static void resolveColumnWidths(WidgetT *w, const BitmapFontT *font) { int32_t colCount = w->as.listView.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 = w->as.listView.cols[c].width; if (taggedW == 0) { // Auto-size: scan data for widest string in this column int32_t maxLen = (int32_t)strlen(w->as.listView.cols[c].title); for (int32_t r = 0; r < w->as.listView.rowCount; r++) { const char *cell = w->as.listView.cellData[r * colCount + c]; if (cell) { int32_t slen = (int32_t)strlen(cell); if (slen > maxLen) { maxLen = slen; } } } w->as.listView.resolvedColW[c] = maxLen * font->charWidth + LISTVIEW_COL_PAD; } else { w->as.listView.resolvedColW[c] = wgtResolveSize(taggedW, parentW, font->charWidth); } totalW += w->as.listView.resolvedColW[c]; } w->as.listView.totalColW = totalW; } // ============================================================ // widgetListViewColBorderHit // ============================================================ // // Returns true if (vx, vy) is on a column header border (resize zone). // Coordinates are in widget/virtual space (scroll-adjusted). bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy) { if (!w || w->type != WidgetListViewE || w->as.listView.colCount == 0) { return false; } AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; 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 - w->as.listView.scrollPosH; for (int32_t c = 0; c < w->as.listView.colCount; c++) { int32_t border = colX + w->as.listView.resolvedColW[c]; if (vx >= border - 3 && vx <= border + 3) { return true; } colX += w->as.listView.resolvedColW[c]; } return false; } // ============================================================ // widgetListViewDestroy // ============================================================ void widgetListViewDestroy(WidgetT *w) { if (w->as.listView.sortIndex) { free(w->as.listView.sortIndex); w->as.listView.sortIndex = NULL; } } // ============================================================ // wgtListView // ============================================================ WidgetT *wgtListView(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, WidgetListViewE); if (w) { w->as.listView.selectedIdx = -1; w->as.listView.sortCol = -1; w->as.listView.sortDir = ListViewSortNoneE; w->weight = 100; } return w; } // ============================================================ // wgtListViewGetSelected // ============================================================ int32_t wgtListViewGetSelected(const WidgetT *w) { if (!w || w->type != WidgetListViewE) { return -1; } return w->as.listView.selectedIdx; } // ============================================================ // wgtListViewSetColumns // ============================================================ void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) { if (!w || w->type != WidgetListViewE) { return; } if (count > LISTVIEW_MAX_COLS) { count = LISTVIEW_MAX_COLS; } w->as.listView.cols = cols; w->as.listView.colCount = count; w->as.listView.totalColW = 0; } // ============================================================ // wgtListViewSetData // ============================================================ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { if (!w || w->type != WidgetListViewE) { return; } // Free old sort index since row count may have changed if (w->as.listView.sortIndex) { free(w->as.listView.sortIndex); w->as.listView.sortIndex = NULL; } w->as.listView.cellData = cellData; w->as.listView.rowCount = rowCount; w->as.listView.totalColW = 0; if (w->as.listView.selectedIdx >= rowCount) { w->as.listView.selectedIdx = rowCount > 0 ? 0 : -1; } if (w->as.listView.selectedIdx < 0 && rowCount > 0) { w->as.listView.selectedIdx = 0; } // Rebuild sort index if sort is active listViewBuildSortIndex(w); } // ============================================================ // wgtListViewSetHeaderClickCallback // ============================================================ void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)) { if (!w || w->type != WidgetListViewE) { return; } w->as.listView.onHeaderClick = cb; } // ============================================================ // wgtListViewSetSelected // ============================================================ void wgtListViewSetSelected(WidgetT *w, int32_t idx) { if (!w || w->type != WidgetListViewE) { return; } w->as.listView.selectedIdx = idx; } // ============================================================ // wgtListViewSetSort // ============================================================ void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir) { if (!w || w->type != WidgetListViewE) { return; } w->as.listView.sortCol = col; w->as.listView.sortDir = dir; listViewBuildSortIndex(w); } // ============================================================ // widgetListViewCalcMinSize // ============================================================ void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t headerH = font->charHeight + 4; int32_t minW = font->charWidth * 12 + LISTVIEW_BORDER * 2 + LISTVIEW_SB_W; w->calcMinW = minW; w->calcMinH = headerH + LISTVIEW_MIN_ROWS * font->charHeight + LISTVIEW_BORDER * 2; } // ============================================================ // widgetListViewOnKey // ============================================================ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)mod; if (!w || w->type != WidgetListViewE || w->as.listView.rowCount == 0) { return; } int32_t rowCount = w->as.listView.rowCount; int32_t *sortIdx = w->as.listView.sortIndex; // Find current display row from selectedIdx (data row) int32_t displaySel = -1; if (w->as.listView.selectedIdx >= 0) { if (sortIdx) { for (int32_t i = 0; i < rowCount; i++) { if (sortIdx[i] == w->as.listView.selectedIdx) { displaySel = i; break; } } } else { displaySel = w->as.listView.selectedIdx; } } // Compute visible rows for page up/down AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; 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; } if (key == (0x50 | 0x100)) { // Down arrow if (displaySel < rowCount - 1) { displaySel++; } else if (displaySel < 0) { displaySel = 0; } } else if (key == (0x48 | 0x100)) { // Up arrow if (displaySel > 0) { displaySel--; } else if (displaySel < 0) { displaySel = 0; } } else if (key == (0x47 | 0x100)) { // Home displaySel = 0; } else if (key == (0x4F | 0x100)) { // End displaySel = rowCount - 1; } else if (key == (0x51 | 0x100)) { // Page Down displaySel += visibleRows; if (displaySel >= rowCount) { displaySel = rowCount - 1; } } else if (key == (0x49 | 0x100)) { // Page Up displaySel -= visibleRows; if (displaySel < 0) { displaySel = 0; } } else { return; } // Convert display row back to data row w->as.listView.selectedIdx = sortIdx ? sortIdx[displaySel] : displaySel; // Scroll to keep selection visible (in display-row space) if (displaySel >= 0) { if (displaySel < w->as.listView.scrollPos) { w->as.listView.scrollPos = displaySel; } else if (displaySel >= w->as.listView.scrollPos + visibleRows) { w->as.listView.scrollPos = displaySel - visibleRows + 1; } } if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); } // ============================================================ // widgetListViewOnMouse // ============================================================ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { hit->focused = true; AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; // Resolve column widths if needed if (hit->as.listView.totalColW == 0 && hit->as.listView.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 = hit->as.listView.totalColW; bool needVSb = (hit->as.listView.rowCount > visibleRows); bool needHSb = false; if (needVSb) { innerW -= LISTVIEW_SB_W; } if (totalColW > innerW) { needHSb = true; innerH -= LISTVIEW_SB_W; visibleRows = innerH / font->charHeight; if (!needVSb && hit->as.listView.rowCount > visibleRows) { needVSb = true; innerW -= LISTVIEW_SB_W; } } if (visibleRows < 1) { visibleRows = 1; } // Clamp scroll positions int32_t maxScrollV = hit->as.listView.rowCount - visibleRows; int32_t maxScrollH = totalColW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } hit->as.listView.scrollPos = clampInt(hit->as.listView.scrollPos, 0, maxScrollV); hit->as.listView.scrollPosH = clampInt(hit->as.listView.scrollPosH, 0, maxScrollH); // Check vertical scrollbar if (needVSb) { int32_t sbX = hit->x + hit->w - LISTVIEW_BORDER - LISTVIEW_SB_W; int32_t sbY = hit->y + LISTVIEW_BORDER + headerH; int32_t sbH = innerH; if (vx >= sbX && vy >= sbY && vy < sbY + sbH) { int32_t relY = vy - sbY; int32_t trackLen = sbH - LISTVIEW_SB_W * 2; if (relY < LISTVIEW_SB_W) { // Up arrow if (hit->as.listView.scrollPos > 0) { hit->as.listView.scrollPos--; } } else if (relY >= sbH - LISTVIEW_SB_W) { // Down arrow if (hit->as.listView.scrollPos < maxScrollV) { hit->as.listView.scrollPos++; } } else if (trackLen > 0) { // Track — page up/down int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, hit->as.listView.rowCount, visibleRows, hit->as.listView.scrollPos, &thumbPos, &thumbSize); int32_t trackRelY = relY - LISTVIEW_SB_W; if (trackRelY < thumbPos) { hit->as.listView.scrollPos -= visibleRows; hit->as.listView.scrollPos = clampInt(hit->as.listView.scrollPos, 0, maxScrollV); } else if (trackRelY >= thumbPos + thumbSize) { hit->as.listView.scrollPos += visibleRows; hit->as.listView.scrollPos = clampInt(hit->as.listView.scrollPos, 0, maxScrollV); } } return; } } // Check horizontal scrollbar if (needHSb) { int32_t sbX = hit->x + LISTVIEW_BORDER; int32_t sbY = hit->y + hit->h - LISTVIEW_BORDER - LISTVIEW_SB_W; int32_t sbW = innerW; if (vy >= sbY && vx >= sbX && vx < sbX + sbW) { int32_t relX = vx - sbX; int32_t trackLen = sbW - LISTVIEW_SB_W * 2; int32_t pageSize = innerW - font->charWidth; if (pageSize < font->charWidth) { pageSize = font->charWidth; } if (relX < LISTVIEW_SB_W) { // Left arrow hit->as.listView.scrollPosH -= font->charWidth; } else if (relX >= sbW - LISTVIEW_SB_W) { // Right arrow hit->as.listView.scrollPosH += font->charWidth; } else if (trackLen > 0) { // Track — page left/right int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalColW, innerW, hit->as.listView.scrollPosH, &thumbPos, &thumbSize); int32_t trackRelX = relX - LISTVIEW_SB_W; if (trackRelX < thumbPos) { hit->as.listView.scrollPosH -= pageSize; } else if (trackRelX >= thumbPos + thumbSize) { hit->as.listView.scrollPosH += pageSize; } } hit->as.listView.scrollPosH = clampInt(hit->as.listView.scrollPosH, 0, maxScrollH); return; } } // Check dead corner (both scrollbars present) if (needVSb && needHSb) { int32_t cornerX = hit->x + hit->w - LISTVIEW_BORDER - LISTVIEW_SB_W; int32_t cornerY = hit->y + hit->h - LISTVIEW_BORDER - LISTVIEW_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 - hit->as.listView.scrollPosH; for (int32_t c = 0; c < hit->as.listView.colCount; c++) { int32_t cw = hit->as.listView.resolvedColW[c]; int32_t border = colX + cw; if (vx >= border - 3 && vx <= border + 3 && c < hit->as.listView.colCount) { // Start column resize drag sResizeListView = hit; sResizeCol = c; sResizeStartX = vx; sResizeOrigW = cw; return; } colX += cw; } // Not on a border — check for sort click colX = hit->x + LISTVIEW_BORDER - hit->as.listView.scrollPosH; for (int32_t c = 0; c < hit->as.listView.colCount; c++) { int32_t cw = hit->as.listView.resolvedColW[c]; if (vx >= colX && vx < colX + cw) { // Toggle sort direction for this column if (hit->as.listView.sortCol == c) { if (hit->as.listView.sortDir == ListViewSortAscE) { hit->as.listView.sortDir = ListViewSortDescE; } else { hit->as.listView.sortDir = ListViewSortAscE; } } else { hit->as.listView.sortCol = c; hit->as.listView.sortDir = ListViewSortAscE; } listViewBuildSortIndex(hit); if (hit->as.listView.onHeaderClick) { hit->as.listView.onHeaderClick(hit, c, hit->as.listView.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 = hit->as.listView.scrollPos + clickedRow; if (displayRow >= 0 && displayRow < hit->as.listView.rowCount) { int32_t dataRow = hit->as.listView.sortIndex ? hit->as.listView.sortIndex[displayRow] : displayRow; hit->as.listView.selectedIdx = dataRow; if (hit->onChange) { hit->onChange(hit); } } wgtInvalidatePaint(hit); } // ============================================================ // widgetListViewPaint // ============================================================ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; // Resolve column widths if needed if (w->as.listView.totalColW == 0 && w->as.listView.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 = w->as.listView.totalColW; bool needVSb = (w->as.listView.rowCount > visibleRows); bool needHSb = false; if (needVSb) { innerW -= LISTVIEW_SB_W; } if (totalColW > innerW) { needHSb = true; innerH -= LISTVIEW_SB_W; visibleRows = innerH / font->charHeight; if (!needVSb && w->as.listView.rowCount > visibleRows) { needVSb = true; innerW -= LISTVIEW_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 = w->as.listView.rowCount - visibleRows; int32_t maxScrollH = totalColW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } w->as.listView.scrollPos = clampInt(w->as.listView.scrollPos, 0, maxScrollV); w->as.listView.scrollPosH = clampInt(w->as.listView.scrollPosH, 0, maxScrollH); int32_t baseX = w->x + LISTVIEW_BORDER; int32_t baseY = w->y + LISTVIEW_BORDER; int32_t colCount = w->as.listView.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 - w->as.listView.scrollPosH; BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1); for (int32_t c = 0; c < colCount; c++) { int32_t cw = w->as.listView.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 == w->as.listView.sortCol && w->as.listView.sortDir != ListViewSortNoneE) { availTextW -= LISTVIEW_SORT_W; } if (w->as.listView.cols[c].title) { int32_t titleLen = (int32_t)strlen(w->as.listView.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, w->as.listView.cols[c].title, titleLen, colors->contentFg, colors->windowFace, true); } } // Sort indicator if (c == w->as.listView.sortCol && w->as.listView.sortDir != ListViewSortNoneE) { int32_t cx = hdrX + cw - LISTVIEW_SORT_W / 2 - LISTVIEW_PAD; int32_t cy = baseY + headerH / 2; if (w->as.listView.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 = w->as.listView.scrollPos; int32_t *sortIdx = w->as.listView.sortIndex; // Fill entire data area background first rectFill(d, ops, baseX, dataY, innerW, innerH, bg); for (int32_t i = 0; i < visibleRows && (scrollPos + i) < w->as.listView.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; if (dataRow == w->as.listView.selectedIdx) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; rectFill(d, ops, baseX, iy, innerW, font->charHeight, ibg); } // Draw each cell int32_t cellX = baseX - w->as.listView.scrollPosH; for (int32_t c = 0; c < colCount; c++) { int32_t cw = w->as.listView.resolvedColW[c]; const char *cell = w->as.listView.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; if (w->as.listView.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 (w->as.listView.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; } } setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); } // ---- Draw scrollbars ---- if (needVSb) { int32_t sbX = w->x + w->w - LISTVIEW_BORDER - LISTVIEW_SB_W; int32_t sbY = w->y + LISTVIEW_BORDER + headerH; drawListViewVScrollbar(w, d, ops, colors, sbX, sbY, innerH, w->as.listView.rowCount, visibleRows); } if (needHSb) { int32_t sbX = w->x + LISTVIEW_BORDER; int32_t sbY = w->y + w->h - LISTVIEW_BORDER - LISTVIEW_SB_W; drawListViewHScrollbar(w, d, ops, colors, sbX, sbY, innerW, totalColW, innerW); // Fill dead corner when both scrollbars present if (needVSb) { rectFill(d, ops, sbX + innerW, sbY, LISTVIEW_SB_W, LISTVIEW_SB_W, colors->windowFace); } } if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } }