1321 lines
41 KiB
C
1321 lines
41 KiB
C
// widgetListView.c — ListView (multi-column list) widget
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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 allocListViewSelBits(WidgetT *w);
|
|
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);
|
|
|
|
|
|
// ============================================================
|
|
// allocListViewSelBits
|
|
// ============================================================
|
|
|
|
static void allocListViewSelBits(WidgetT *w) {
|
|
if (w->as.listView.selBits) {
|
|
free(w->as.listView.selBits);
|
|
w->as.listView.selBits = NULL;
|
|
}
|
|
|
|
int32_t count = w->as.listView.rowCount;
|
|
|
|
if (count > 0 && w->as.listView.multiSelect) {
|
|
w->as.listView.selBits = (uint8_t *)calloc(count, 1);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
if (w->as.listView.selBits) {
|
|
free(w->as.listView.selBits);
|
|
w->as.listView.selBits = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListView
|
|
// ============================================================
|
|
|
|
WidgetT *wgtListView(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, WidgetListViewE);
|
|
|
|
if (w) {
|
|
w->as.listView.selectedIdx = -1;
|
|
w->as.listView.anchorIdx = -1;
|
|
w->as.listView.sortCol = -1;
|
|
w->as.listView.sortDir = ListViewSortNoneE;
|
|
w->as.listView.dragIdx = -1;
|
|
w->as.listView.dropIdx = -1;
|
|
w->weight = 100;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewGetSelected
|
|
// ============================================================
|
|
|
|
int32_t wgtListViewGetSelected(const WidgetT *w) {
|
|
if (!w || w->type != WidgetListViewE) {
|
|
return -1;
|
|
}
|
|
|
|
return w->as.listView.selectedIdx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewClearSelection
|
|
// ============================================================
|
|
|
|
void wgtListViewClearSelection(WidgetT *w) {
|
|
if (!w || w->type != WidgetListViewE || !w->as.listView.selBits) {
|
|
return;
|
|
}
|
|
|
|
memset(w->as.listView.selBits, 0, w->as.listView.rowCount);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewIsItemSelected
|
|
// ============================================================
|
|
|
|
bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx) {
|
|
if (!w || w->type != WidgetListViewE) {
|
|
return false;
|
|
}
|
|
|
|
if (!w->as.listView.multiSelect) {
|
|
return idx == w->as.listView.selectedIdx;
|
|
}
|
|
|
|
if (!w->as.listView.selBits || idx < 0 || idx >= w->as.listView.rowCount) {
|
|
return false;
|
|
}
|
|
|
|
return w->as.listView.selBits[idx] != 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewSelectAll
|
|
// ============================================================
|
|
|
|
void wgtListViewSelectAll(WidgetT *w) {
|
|
if (!w || w->type != WidgetListViewE || !w->as.listView.selBits) {
|
|
return;
|
|
}
|
|
|
|
memset(w->as.listView.selBits, 1, w->as.listView.rowCount);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
w->as.listView.anchorIdx = w->as.listView.selectedIdx;
|
|
|
|
// Rebuild sort index if sort is active
|
|
listViewBuildSortIndex(w);
|
|
|
|
// Reallocate selection bits
|
|
allocListViewSelBits(w);
|
|
|
|
if (w->as.listView.selBits && w->as.listView.selectedIdx >= 0) {
|
|
w->as.listView.selBits[w->as.listView.selectedIdx] = 1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewSetItemSelected
|
|
// ============================================================
|
|
|
|
void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
|
if (!w || w->type != WidgetListViewE) {
|
|
return;
|
|
}
|
|
|
|
if (!w->as.listView.selBits || idx < 0 || idx >= w->as.listView.rowCount) {
|
|
return;
|
|
}
|
|
|
|
w->as.listView.selBits[idx] = selected ? 1 : 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewSetReorderable
|
|
// ============================================================
|
|
|
|
void wgtListViewSetReorderable(WidgetT *w, bool reorderable) {
|
|
if (!w || w->type != WidgetListViewE) {
|
|
return;
|
|
}
|
|
|
|
w->as.listView.reorderable = reorderable;
|
|
|
|
// Disable sorting when reorderable — sort order conflicts with manual order
|
|
if (reorderable) {
|
|
w->as.listView.sortCol = -1;
|
|
w->as.listView.sortDir = ListViewSortNoneE;
|
|
|
|
if (w->as.listView.sortIndex) {
|
|
free(w->as.listView.sortIndex);
|
|
w->as.listView.sortIndex = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewSetMultiSelect
|
|
// ============================================================
|
|
|
|
void wgtListViewSetMultiSelect(WidgetT *w, bool multi) {
|
|
if (!w || w->type != WidgetListViewE) {
|
|
return;
|
|
}
|
|
|
|
w->as.listView.multiSelect = multi;
|
|
allocListViewSelBits(w);
|
|
|
|
if (w->as.listView.selBits && w->as.listView.selectedIdx >= 0) {
|
|
w->as.listView.selBits[w->as.listView.selectedIdx] = 1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListViewSetSelected
|
|
// ============================================================
|
|
|
|
void wgtListViewSetSelected(WidgetT *w, int32_t idx) {
|
|
if (!w || w->type != WidgetListViewE) {
|
|
return;
|
|
}
|
|
|
|
w->as.listView.selectedIdx = idx;
|
|
w->as.listView.anchorIdx = idx;
|
|
|
|
if (w->as.listView.selBits) {
|
|
memset(w->as.listView.selBits, 0, w->as.listView.rowCount);
|
|
|
|
if (idx >= 0 && idx < w->as.listView.rowCount) {
|
|
w->as.listView.selBits[idx] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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) {
|
|
if (!w || w->type != WidgetListViewE || w->as.listView.rowCount == 0) {
|
|
return;
|
|
}
|
|
|
|
bool multi = w->as.listView.multiSelect;
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (mod & KEY_MOD_CTRL) != 0;
|
|
int32_t rowCount = w->as.listView.rowCount;
|
|
int32_t *sortIdx = w->as.listView.sortIndex;
|
|
|
|
// Ctrl+A — select all (multi-select only)
|
|
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
|
|
wgtListViewSelectAll(w);
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Space — toggle current item (multi-select only)
|
|
if (multi && key == ' ') {
|
|
int32_t sel = w->as.listView.selectedIdx;
|
|
|
|
if (sel >= 0 && w->as.listView.selBits) {
|
|
w->as.listView.selBits[sel] ^= 1;
|
|
w->as.listView.anchorIdx = sel;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// 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
|
|
int32_t newDataRow = sortIdx ? sortIdx[displaySel] : displaySel;
|
|
|
|
if (newDataRow == w->as.listView.selectedIdx) {
|
|
return;
|
|
}
|
|
|
|
w->as.listView.selectedIdx = newDataRow;
|
|
|
|
// Update multi-select
|
|
if (multi && w->as.listView.selBits) {
|
|
if (shift) {
|
|
// Shift+arrow: range from anchor to new cursor (in data-row space)
|
|
memset(w->as.listView.selBits, 0, rowCount);
|
|
|
|
// Convert anchor to display row, then select display range mapped to data rows
|
|
int32_t anchorDisplay = -1;
|
|
int32_t anchor = w->as.listView.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;
|
|
w->as.listView.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 < 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 (disabled when reorderable)
|
|
if (!hit->as.listView.reorderable) {
|
|
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;
|
|
|
|
bool multi = hit->as.listView.multiSelect;
|
|
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
|
|
|
|
if (multi && hit->as.listView.selBits) {
|
|
if (ctrl) {
|
|
// Ctrl+click: toggle item, update anchor
|
|
hit->as.listView.selBits[dataRow] ^= 1;
|
|
hit->as.listView.anchorIdx = dataRow;
|
|
} else if (shift) {
|
|
// Shift+click: range from anchor to clicked (in display-row space)
|
|
memset(hit->as.listView.selBits, 0, hit->as.listView.rowCount);
|
|
|
|
int32_t anchorDisplay = -1;
|
|
int32_t anchor = hit->as.listView.anchorIdx;
|
|
|
|
if (hit->as.listView.sortIndex && anchor >= 0) {
|
|
for (int32_t i = 0; i < hit->as.listView.rowCount; i++) {
|
|
if (hit->as.listView.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 >= hit->as.listView.rowCount) {
|
|
hi = hit->as.listView.rowCount - 1;
|
|
}
|
|
|
|
for (int32_t i = lo; i <= hi; i++) {
|
|
int32_t dr = hit->as.listView.sortIndex ? hit->as.listView.sortIndex[i] : i;
|
|
hit->as.listView.selBits[dr] = 1;
|
|
}
|
|
} else {
|
|
// Plain click: select only this item, update anchor
|
|
memset(hit->as.listView.selBits, 0, hit->as.listView.rowCount);
|
|
hit->as.listView.selBits[dataRow] = 1;
|
|
hit->as.listView.anchorIdx = dataRow;
|
|
}
|
|
}
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
|
|
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
|
hit->onDblClick(hit);
|
|
}
|
|
|
|
// Initiate drag-reorder if enabled (not from modifier clicks)
|
|
if (hit->as.listView.reorderable && !shift && !ctrl) {
|
|
hit->as.listView.dragIdx = dataRow;
|
|
hit->as.listView.dropIdx = dataRow;
|
|
sDragReorder = 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);
|
|
|
|
bool multi = w->as.listView.multiSelect;
|
|
uint8_t *selBits = w->as.listView.selBits;
|
|
|
|
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;
|
|
|
|
bool selected = false;
|
|
|
|
if (multi && selBits) {
|
|
selected = selBits[dataRow] != 0;
|
|
} else {
|
|
selected = (dataRow == w->as.listView.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 - 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;
|
|
}
|
|
|
|
// Draw cursor focus rect in multi-select mode (on top of text)
|
|
if (multi && dataRow == w->as.listView.selectedIdx && w->focused) {
|
|
drawFocusRect(d, ops, baseX, iy, innerW, font->charHeight, fg);
|
|
}
|
|
}
|
|
|
|
// Draw drag-reorder insertion indicator
|
|
if (w->as.listView.reorderable && w->as.listView.dragIdx >= 0 && w->as.listView.dropIdx >= 0) {
|
|
int32_t drop = w->as.listView.dropIdx;
|
|
int32_t lineY = dataY + (drop - w->as.listView.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 - 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);
|
|
}
|
|
}
|