// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#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 "../../../libs/kpunch/listhelp/listHelp.h"

#define LISTVIEW_BORDER      2
#define LISTVIEW_MIN_COL_W   20

static int32_t sTypeId = -1;

#include <stdlib.h>
#include <string.h>
#include "stb_ds_wrap.h"

#define LISTVIEW_PAD          3
#define LISTVIEW_MIN_ROWS     4
#define LISTVIEW_COL_PAD      6
#define LISTVIEW_SORT_W       10

// Header row vertical padding: font->charHeight + LISTVIEW_HEADER_VPAD.
#define LISTVIEW_HEADER_VPAD  4
// Column-border drag hit-zone radius (pixels to either side of the border).
#define LISTVIEW_RESIZE_ZONE  3
// Header-text vertical offset inside a header cell.
#define LISTVIEW_HEADER_TEXT_YOFF 2
// Sort-indicator triangle glyph (centered on column header).
#define LISTVIEW_SORT_TRI_HALF_W  3   // half-width of the widest row
#define LISTVIEW_SORT_TRI_HEIGHT  4   // rows in triangle glyph
#define LISTVIEW_SORT_TRI_BASE    (2 * LISTVIEW_SORT_TRI_HALF_W + 1)   // base-row width

// 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 {
    // Fields are ordered by alignment to minimize struct padding.
    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;    // heap-alloced, sized to colCount
    int32_t             resolvedColCap;  // allocated capacity
    int32_t             totalColW;
    int32_t            *sortIndex;
    int32_t             anchorIdx;
    uint8_t            *selBits;
    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;
    int32_t             sbDragOrient;
    int32_t             sbDragOff;
    // Owned cell storage for AddItem/RemoveRow/Clear/SetCell
    char               **ownedCells;   // stb_ds dynamic array of strdup'd strings
    int32_t              nextCell;     // next cell position for AddItem (row-major index)
    // Owned column definitions (deep copies for BASIC callers)
    ListViewColT        *ownedColDefs; // heap array of column defs (NULL if not owned)
    char               **ownedColTitles; // heap array of strdup'd title strings
    int32_t              ownedColCount;
    // Flags (grouped last to minimize struct padding)
    bool                multiSelect;
    bool                reorderable;
    bool                resizeDragging;
    bool                ownsCells;    // true if cellData points to ownedCells
} ListViewDataT;

// BASIC wrapper: SetColumns "Name,100|Description,200"
// No fixed cap: columns allocated dynamically based on parsed count.


// ============================================================
// Prototypes
// ============================================================

static void    allocListViewSelBits(WidgetT *w);
static void    basSetColumns(WidgetT *w, const char *spec);
static void    listViewAdoptCells(WidgetT *w);
static void    listViewBuildSortIndex(WidgetT *w);
static void    listViewRemapOwnedCells(WidgetT *w, int32_t newColCount);
static void    listViewSyncOwned(WidgetT *w);
static void    resolveColumnWidths(WidgetT *w, const BitmapFontT *font);
WidgetT       *wgtListView(WidgetT *parent);
void           wgtListViewAddItem(WidgetT *w, const char *text);
void           wgtListViewClear(WidgetT *w);
void           wgtListViewClearSelection(WidgetT *w);
const char    *wgtListViewGetCell(const WidgetT *w, int32_t row, int32_t col);
int32_t        wgtListViewGetRowCount(const WidgetT *w);
int32_t        wgtListViewGetSelected(const WidgetT *w);
bool           wgtListViewIsItemSelected(const WidgetT *w, int32_t idx);
void           wgtListViewRemoveRow(WidgetT *w, int32_t row);
void           wgtListViewSelectAll(WidgetT *w);
void           wgtListViewSetCell(WidgetT *w, int32_t row, int32_t col, const char *text);
void           wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count);
void           wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount);
void           wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir));
void           wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected);
void           wgtListViewSetMultiSelect(WidgetT *w, bool multi);
void           wgtListViewSetReorderable(WidgetT *w, bool reorderable);
void           wgtListViewSetSelected(WidgetT *w, int32_t idx);
void           wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir);
void           wgtRegister(void);
void           widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font);
bool           widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy);
void           widgetListViewDestroy(WidgetT *w);
int32_t        widgetListViewGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy);
static void    widgetListViewOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void    widgetListViewOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
void           widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod);
void           widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
void           widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
static void    widgetListViewReorderDrop(WidgetT *w);
static void    widgetListViewReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void    widgetListViewScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);


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);
    }
}


static void basSetColumns(WidgetT *w, const char *spec) {
    if (!spec || !*spec) {
        return;
    }

    VALIDATE_WIDGET_VOID(w, sTypeId);
    ListViewDataT *lv = (ListViewDataT *)w->data;

    // Free previous owned columns
    for (int32_t i = 0; i < lv->ownedColCount; i++) {
        free(lv->ownedColTitles[i]);
    }

    free(lv->ownedColTitles);
    free(lv->ownedColDefs);
    lv->ownedColTitles = NULL;
    lv->ownedColDefs   = NULL;
    lv->ownedColCount  = 0;

    // lv->cols was aliased to the just-freed ownedColDefs by a prior
    // wgtListViewSetColumns, so clear the live alias to avoid a dangling read.
    lv->cols           = NULL;
    lv->colCount       = 0;

    // Parse spec into a temporary buffer
    char buf[1024];
    snprintf(buf, sizeof(buf), "%s", spec);

    // First pass: count columns
    int32_t count = 0;
    char *p = buf;

    while (*p) {
        count++;
        char *sep = strchr(p, '|');

        if (sep) {
            p = sep + 1;
        } else {
            break;
        }
    }

    if (count <= 0) {
        return;
    }

    // Allocate owned storage
    lv->ownedColDefs   = (ListViewColT *)calloc(count, sizeof(ListViewColT));
    lv->ownedColTitles = (char **)calloc(count, sizeof(char *));

    if (!lv->ownedColDefs || !lv->ownedColTitles) {
        free(lv->ownedColDefs);
        free(lv->ownedColTitles);
        lv->ownedColDefs   = NULL;
        lv->ownedColTitles = NULL;
        return;
    }

    // Second pass: parse and deep-copy
    snprintf(buf, sizeof(buf), "%s", spec);
    int32_t  idx = 0;
    char    *tok = buf;

    while (*tok && idx < count) {
        char *sep = strchr(tok, '|');

        if (sep) {
            *sep = '\0';
        }

        char *comma = strchr(tok, ',');

        if (comma) {
            *comma = '\0';
            lv->ownedColTitles[idx]    = strdup(tok);
            lv->ownedColDefs[idx].title = lv->ownedColTitles[idx];
            lv->ownedColDefs[idx].width = wgtChars(atoi(comma + 1));
            lv->ownedColDefs[idx].align = ListViewAlignLeftE;
        } else {
            lv->ownedColTitles[idx]    = strdup(tok);
            lv->ownedColDefs[idx].title = lv->ownedColTitles[idx];
            lv->ownedColDefs[idx].width = 0;
            lv->ownedColDefs[idx].align = ListViewAlignLeftE;
        }

        idx++;

        if (sep) {
            tok = sep + 1;
        } else {
            break;
        }
    }

    lv->ownedColCount = idx;
    wgtListViewSetColumns(w, lv->ownedColDefs, idx);
}


// 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;
    }

    // Always size to the current rowCount.  The buffer is fully
    // re-initialized below, but it must be (re)grown when rows were
    // added since the last build, or the identity-init and insertion
    // sort would write past an undersized allocation (heap overflow).
    int32_t *idx = (int32_t *)realloc(lv->sortIndex, (size_t)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;
    }
}


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


// Copy the current (application-owned, external) cell data into the internally
// owned storage so the list view may rearrange it.  A reorderable list view
// MUST own its cells: drag-reorder rewrites the cell array in place and must
// not mutate a caller-owned SetData array (which may be const/shared), so
// widgetListViewReorderDrop refuses to move rows unless ownsCells is true.
// The strdup adoption sequence itself lives in the shared
// widgetAdoptOwnedStrings helper (listHelp), shared with ListBox.
// No-op when already owned.  Preserves the current row order and selection.
static void listViewAdoptCells(WidgetT *w) {
    ListViewDataT *lv = (ListViewDataT *)w->data;

    if (lv->ownsCells) {
        return;
    }

    // Columns may not be set yet (SetData before SetColumns is legal).
    // Without a column count the row-major cell count is unknowable, so
    // adopting here would discard the external data while rowCount stays
    // nonzero (paint would then index a NULL/empty owned array once the
    // columns arrive).  Defer -- wgtListViewSetColumns adopts once the
    // first column count is known.
    if (lv->colCount <= 0) {
        return;
    }

    int32_t cellCount = lv->rowCount * lv->colCount;

    lv->ownedCells = widgetAdoptOwnedStrings(lv->ownedCells, lv->cellData, cellCount);
    lv->cellData   = (const char **)lv->ownedCells;
    lv->nextCell   = cellCount;
    lv->ownsCells  = true;
}


// Remap the owned row-major cell array to a new column count, preserving
// rows.  Cells beyond a narrower row are freed; a wider row is padded with
// empty strings.  Without this, changing the column count after rows exist
// leaves rowCount * colCount larger than the cell array and paint/autosize
// index past its end.
static void listViewRemapOwnedCells(WidgetT *w, int32_t newColCount) {
    ListViewDataT *lv        = (ListViewDataT *)w->data;
    int32_t        cellCount = (int32_t)arrlen(lv->ownedCells);

    if (lv->rowCount <= 0 || newColCount <= 0) {
        return;
    }

    int32_t oldColCount = cellCount / lv->rowCount;

    if (oldColCount == newColCount) {
        return;
    }

    char **remapped = NULL;

    for (int32_t r = 0; r < lv->rowCount; r++) {
        for (int32_t c = 0; c < newColCount; c++) {
            if (c < oldColCount) {
                arrput(remapped, lv->ownedCells[r * oldColCount + c]);
            } else {
                arrput(remapped, strdup(""));
            }
        }

        for (int32_t c = newColCount; c < oldColCount; c++) {
            free(lv->ownedCells[r * oldColCount + c]);
        }
    }

    arrfree(lv->ownedCells);
    lv->ownedCells = remapped;
    lv->cellData   = (const char **)lv->ownedCells;
    lv->nextCell   = lv->rowCount * newColCount;
}


// 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;
}


WidgetT *wgtListView(WidgetT *parent) {
    WidgetT *w = widgetAlloc(parent, sTypeId);

    if (!w) {
        return NULL;
    }

    ListViewDataT *lv = (ListViewDataT *)calloc(1, sizeof(ListViewDataT));

    if (!lv) {
        // Detach from the parent child list + clear core references before
        // freeing, matching slider/canvas/listBox; a plain free(w) would
        // leave a dangling child pointer in the parent.
        widgetAllocRollback(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       = WGT_WEIGHT_FILL;

    return w;
}


void wgtListViewAddItem(WidgetT *w, const char *text) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ListViewDataT *lv = (ListViewDataT *)w->data;

    if (lv->colCount <= 0) {
        return;
    }

    // Always align to the next row boundary so AddItem always starts a new row,
    // even if SetCell was used to fill remaining columns of the previous row.
    int32_t rem = lv->nextCell % lv->colCount;

    if (rem != 0) {
        lv->nextCell += lv->colCount - rem;
    }

    // Add colCount empty cells for the new row
    for (int32_t c = 0; c < lv->colCount; c++) {
        arrput(lv->ownedCells, strdup(""));
    }

    // Set the first cell of the new row to 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;
    }

    // Rows backed by external SetData (ownsCells == false) have no
    // ownedCells array; removing would deref NULL.  Refuse rather than
    // crash -- callers should manage externally-supplied data themselves.
    if (!lv->ownsCells || !lv->ownedCells) {
        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 wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count) {
    VALIDATE_WIDGET_VOID(w, sTypeId);
    ListViewDataT *lv          = (ListViewDataT *)w->data;
    int32_t        oldColCount = lv->colCount;

    // Grow resolvedColW as needed
    if (count > lv->resolvedColCap) {
        int32_t *grown = (int32_t *)realloc(lv->resolvedColW, sizeof(int32_t) * count);

        if (grown) {
            lv->resolvedColW   = grown;
            lv->resolvedColCap = count;
        } else {
            // Allocation failed; clamp count to what we have
            count = lv->resolvedColCap;
        }
    }

    lv->cols      = cols;
    lv->colCount  = count;
    lv->totalColW = 0;

    // A different column count invalidates the row-major cell layout;
    // without reconciliation, paint and autosize would index rowCount *
    // colCount entries into a smaller cell array.
    if (lv->ownsCells) {
        listViewRemapOwnedCells(w, count);
    } else if (oldColCount == 0) {
        // First column layout: an external SetData array supplied before
        // any columns existed is laid out for THIS count, so keep it.
        // A reorderable list view deferred adoption in listViewAdoptCells
        // while the column count was unknown; adopt now.
        if (lv->reorderable && lv->rowCount > 0 && count > 0) {
            listViewAdoptCells(w);
        }
    } else if (count != oldColCount && lv->rowCount > 0) {
        // External SetData arrays were laid out for the previous column
        // count and cannot be remapped here; drop the stale reference and
        // require a fresh wgtListViewSetData for the new layout.
        lv->cellData = NULL;
        lv->rowCount = 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;
    lv->ownsCells = false;

    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);
        }
    }

    // A reorderable list view must own its cells so drag-reorder can rearrange
    // them (see listViewAdoptCells); copy the just-set external data in.
    if (lv->reorderable) {
        listViewAdoptCells(w);
    }

    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 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;
        }

        // Own the cells so drag-reorder can rearrange them even when the data
        // was supplied externally via wgtListViewSetData.
        listViewAdoptCells(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);
}


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);
}


void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    int32_t headerH = font->charHeight + LISTVIEW_HEADER_VPAD;
    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;
}


// 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 + LISTVIEW_HEADER_VPAD;
    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 - LISTVIEW_RESIZE_ZONE && vx <= border + LISTVIEW_RESIZE_ZONE) {
            return true;
        }

        colX += lv->resolvedColW[c];
    }

    return false;
}


// 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);

    for (int32_t i = 0; i < lv->ownedColCount; i++) {
        free(lv->ownedColTitles[i]);
    }
    free(lv->ownedColTitles);
    free(lv->ownedColDefs);

    free(lv->resolvedColW);
    free(lv->selBits);
    free(lv->sortIndex);
    free(lv);
    w->data = NULL;
}


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 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);
}


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);
    }
}


// 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) {
    if (!w || w->type != sTypeId) {
        return;
    }

    ListViewDataT *lv = (ListViewDataT *)w->data;

    if (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 + LISTVIEW_HEADER_VPAD;
    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);

    // Type-ahead: search first column for next matching row
    if (newDisplaySel < 0 && key >= 0x20 && key < 0x7F && lv->cellData && lv->colCount > 0) {
        char upper = (char)key;

        if (upper >= 'a' && upper <= 'z') {
            upper -= 32;
        }

        for (int32_t i = 1; i <= rowCount; i++) {
            int32_t dispRow  = (displaySel + i) % rowCount;
            int32_t dataRow  = sortIdx ? sortIdx[dispRow] : dispRow;
            const char *cell = lv->cellData[dataRow * lv->colCount];

            if (cell && cell[0]) {
                char first = cell[0];

                if (first >= 'a' && first <= 'z') {
                    first -= 32;
                }

                if (first == upper) {
                    newDisplaySel = dispRow;
                    break;
                }
            }
        }
    }

    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);
}


// 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 + LISTVIEW_HEADER_VPAD;
    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 - LISTVIEW_RESIZE_ZONE && vx <= border + LISTVIEW_RESIZE_ZONE) {
                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);
}


// 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 + LISTVIEW_HEADER_VPAD;
    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 + LISTVIEW_HEADER_TEXT_YOFF;
            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): narrow row at top, growing
                    // wider toward the bottom. Row i has width 2*i + 1.
                    for (int32_t i = 0; i < LISTVIEW_SORT_TRI_HEIGHT; i++) {
                        drawHLine(d, ops, cx - i, cy - (LISTVIEW_SORT_TRI_HEIGHT / 2) + i, 1 + i * 2, colors->contentFg);
                    }
                } else {
                    // Down triangle (descending): wide row at top, shrinking
                    // toward the bottom. Row i has width BASE - 2*i.
                    for (int32_t i = 0; i < LISTVIEW_SORT_TRI_HEIGHT; i++) {
                        drawHLine(d, ops, cx - LISTVIEW_SORT_TRI_HALF_W + i, cy - (LISTVIEW_SORT_TRI_HEIGHT / 2 - 1) + i, LISTVIEW_SORT_TRI_BASE - 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);
    }
}


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;
    }

    // Reorder rewrites cellData in place, which is only safe for the
    // internally-owned ownedCells array.  When cellData points at an
    // application-owned array (SetData-fed lists, ownsCells == false) the
    // contract forbids rearranging it, so drag-reorder is a no-op.
    if (!lv->ownsCells) {
        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);
    }
}


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 + LISTVIEW_HEADER_VPAD;
    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++;
    }
}


// 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 + LISTVIEW_HEADER_VPAD;
    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 sbY      = w->y + LISTVIEW_BORDER + headerH;
        int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;

        lv->scrollPos = widgetScrollbarThumbDragScroll(trackLen, lv->rowCount, visibleRows, relMouse, 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 sbX      = w->x + LISTVIEW_BORDER;
        int32_t relMouse = mouseX - sbX - WGT_SB_W - dragOff;

        lv->scrollPosH = widgetScrollbarThumbDragScroll(trackLen, totalColW, innerW, relMouse, maxScroll);
    }
}


// ============================================================
// DXE registration
// ============================================================

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,
    }
};


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 },
    { "SetColumns",      WGT_SIG_STR,              (void *)basSetColumns },
    { "SetItemSelected", WGT_SIG_INT_BOOL,         (void *)wgtListViewSetItemSelected },
    { "SetMultiSelect",  WGT_SIG_BOOL,             (void *)wgtListViewSetMultiSelect },
    { "SetReorderable",  WGT_SIG_BOOL,             (void *)wgtListViewSetReorderable },
    { "SetSort",         WGT_SIG_INT_INT,          (void *)wgtListViewSetSort },
};

static const WgtIfaceT sIface = {
    .basName      = "ListView",
    .props        = sProps,
    .propCount    = sizeof(sProps) / sizeof(sProps[0]),
    .methods      = sMethods,
    .methodCount  = sizeof(sMethods) / sizeof(sMethods[0]),
    .events       = NULL,
    .eventCount   = 0,
    .createSig    = WGT_CREATE_PARENT,
    .defaultEvent = "Click"
};

void wgtRegister(void) {
    sTypeId = wgtRegisterClass(&sClassListView);
    wgtRegisterApi("listview", &sApi);
    wgtRegisterIface("listview", &sIface);
}
