From d525798836ec4141f0a1a1f46f145fae6905f393 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 15 Mar 2026 21:39:00 -0500 Subject: [PATCH] Multi-select list boxes. Mouse selection when scrolled error fixed. --- dvx/dvxApp.c | 1 + dvx/dvxApp.h | 1 + dvx/dvxWidget.h | 18 +- dvx/widgets/widgetClass.c | 2 +- dvx/widgets/widgetEvent.c | 36 +--- dvx/widgets/widgetInternal.h | 1 + dvx/widgets/widgetListBox.c | 367 +++++++++++++++++++++++++++++++---- dvx/widgets/widgetListView.c | 271 +++++++++++++++++++++++++- dvxdemo/demo.c | 9 +- 9 files changed, 637 insertions(+), 69 deletions(-) diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index 4be1a3c..f1cba76 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -1701,6 +1701,7 @@ static void pollKeyboard(AppContextT *ctx) { r.x.ax = 0x1200; __dpmi_int(0x16, &r); int32_t shiftFlags = r.x.ax & 0xFF; + ctx->keyModifiers = shiftFlags; bool shiftHeld = (shiftFlags & 0x03) != 0; // left or right shift // Process buffered keys diff --git a/dvx/dvxApp.h b/dvx/dvxApp.h index 664e622..e347f7b 100644 --- a/dvx/dvxApp.h +++ b/dvx/dvxApp.h @@ -32,6 +32,7 @@ typedef struct AppContextT { int32_t mouseX; int32_t mouseY; int32_t mouseButtons; + int32_t keyModifiers; // current BIOS shift state (KEY_MOD_xxx) int32_t prevMouseX; int32_t prevMouseY; int32_t prevMouseButtons; diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 8fcf93a..b239df8 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -247,9 +247,12 @@ typedef struct WidgetT { struct { const char **items; int32_t itemCount; - int32_t selectedIdx; + int32_t selectedIdx; // cursor position (always valid); also sole selection in single-select int32_t scrollPos; int32_t maxItemLen; // cached max strlen of items + bool multiSelect; + int32_t anchorIdx; // anchor for shift+click range selection + uint8_t *selBits; // per-item selection flags (multi-select only) } listBox; struct { @@ -415,6 +418,9 @@ typedef struct WidgetT { int32_t resolvedColW[LISTVIEW_MAX_COLS]; int32_t totalColW; int32_t *sortIndex; + bool multiSelect; + int32_t anchorIdx; + uint8_t *selBits; void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); } listView; } as; @@ -537,6 +543,11 @@ int32_t wgtListViewGetSelected(const WidgetT *w); void wgtListViewSetSelected(WidgetT *w, int32_t idx); void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir); void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)); +void wgtListViewSetMultiSelect(WidgetT *w, bool multi); +bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx); +void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected); +void wgtListViewSelectAll(WidgetT *w); +void wgtListViewClearSelection(WidgetT *w); // ============================================================ // ImageButton @@ -655,6 +666,11 @@ void wgtDestroy(WidgetT *w); void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count); int32_t wgtListBoxGetSelected(const WidgetT *w); void wgtListBoxSetSelected(WidgetT *w, int32_t idx); +void wgtListBoxSetMultiSelect(WidgetT *w, bool multi); +bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx); +void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected); +void wgtListBoxSelectAll(WidgetT *w); +void wgtListBoxClearSelection(WidgetT *w); // ============================================================ // Debug diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c index b4bf272..f3315e0 100644 --- a/dvx/widgets/widgetClass.c +++ b/dvx/widgets/widgetClass.c @@ -131,7 +131,7 @@ static const WidgetClassT sClassListBox = { .layout = NULL, .onMouse = widgetListBoxOnMouse, .onKey = widgetListBoxOnKey, - .destroy = NULL, + .destroy = widgetListBoxDestroy, .getText = NULL, .setText = NULL }; diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 503ef7a..b05d1f7 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -159,11 +159,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Handle text drag-select (mouse move while pressed) if (sDragTextSelect && (buttons & 1)) { - int32_t scrollX = win->hScroll ? win->hScroll->value : 0; - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t vx = x + scrollX; - int32_t vy = y + scrollY; - widgetTextDragUpdate(sDragTextSelect, root, vx, vy); + widgetTextDragUpdate(sDragTextSelect, root, x, y); if (sDragTextSelect->type == WidgetAnsiTermE) { // Fast path: repaint only dirty terminal rows into the @@ -258,8 +254,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Handle ListView column resize drag if (sResizeListView && (buttons & 1)) { - int32_t scrollX = win->hScroll ? win->hScroll->value : 0; - int32_t delta = (x + scrollX) - sResizeStartX; + int32_t delta = x - sResizeStartX; int32_t newW = sResizeOrigW + delta; if (newW < 20) { @@ -293,13 +288,8 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Fire onClick if released over the same button in the same window if (sPressedButton->window == win) { - int32_t scrollX = win->hScroll ? win->hScroll->value : 0; - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t vx = x + scrollX; - int32_t vy = y + scrollY; - - if (vx >= sPressedButton->x && vx < sPressedButton->x + sPressedButton->w && - vy >= sPressedButton->y && vy < sPressedButton->y + sPressedButton->h) { + if (x >= sPressedButton->x && x < sPressedButton->x + sPressedButton->w && + y >= sPressedButton->y && y < sPressedButton->y + sPressedButton->h) { if (sPressedButton->onClick) { sPressedButton->onClick(sPressedButton); } @@ -316,13 +306,8 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { bool over = false; if (sPressedButton->window == win) { - int32_t scrollX = win->hScroll ? win->hScroll->value : 0; - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t vx = x + scrollX; - int32_t vy = y + scrollY; - - over = (vx >= sPressedButton->x && vx < sPressedButton->x + sPressedButton->w && - vy >= sPressedButton->y && vy < sPressedButton->y + sPressedButton->h); + over = (x >= sPressedButton->x && x < sPressedButton->x + sPressedButton->w && + y >= sPressedButton->y && y < sPressedButton->y + sPressedButton->h); } bool curPressed = (sPressedButton->type == WidgetImageButtonE) @@ -418,11 +403,10 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { return; } - // Adjust mouse coordinates for scroll offset - int32_t scrollX = win->hScroll ? win->hScroll->value : 0; - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t vx = x + scrollX; - int32_t vy = y + scrollY; + // Widget positions are already in content-buffer space (widgetOnPaint + // sets root to -scrollX/-scrollY), so use raw content-relative coords. + int32_t vx = x; + int32_t vy = y; WidgetT *hit = widgetHitTest(root, vx, vy); diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index fc30499..89ddbbc 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -241,6 +241,7 @@ void widgetCanvasDestroy(WidgetT *w); void widgetComboBoxDestroy(WidgetT *w); void widgetImageButtonDestroy(WidgetT *w); void widgetImageDestroy(WidgetT *w); +void widgetListBoxDestroy(WidgetT *w); void widgetListViewDestroy(WidgetT *w); void widgetTextAreaDestroy(WidgetT *w); void widgetTextInputDestroy(WidgetT *w); diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index c6bcee2..8321294 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -1,13 +1,107 @@ -// widgetListBox.c — ListBox widget +// widgetListBox.c — ListBox widget (single and multi-select) #include "widgetInternal.h" +#include +#include + #define LISTBOX_BORDER 2 #define LISTBOX_PAD 2 #define LISTBOX_MIN_ROWS 4 #define LISTBOX_SB_W 14 +// ============================================================ +// Prototypes +// ============================================================ + +static void allocSelBits(WidgetT *w); +static void ensureScrollVisible(WidgetT *w, int32_t idx); +static void selectRange(WidgetT *w, int32_t from, int32_t to); + + +// ============================================================ +// allocSelBits +// ============================================================ + +static void allocSelBits(WidgetT *w) { + if (w->as.listBox.selBits) { + free(w->as.listBox.selBits); + w->as.listBox.selBits = NULL; + } + + int32_t count = w->as.listBox.itemCount; + + if (count > 0 && w->as.listBox.multiSelect) { + w->as.listBox.selBits = (uint8_t *)calloc(count, 1); + } +} + + +// ============================================================ +// ensureScrollVisible +// ============================================================ + +static void ensureScrollVisible(WidgetT *w, int32_t idx) { + if (idx < 0) { + return; + } + + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + const BitmapFontT *font = &ctx->font; + int32_t innerH = w->h - LISTBOX_BORDER * 2; + int32_t visibleRows = innerH / font->charHeight; + + if (visibleRows < 1) { + visibleRows = 1; + } + + if (idx < w->as.listBox.scrollPos) { + w->as.listBox.scrollPos = idx; + } else if (idx >= w->as.listBox.scrollPos + visibleRows) { + w->as.listBox.scrollPos = idx - visibleRows + 1; + } +} + + +// ============================================================ +// selectRange +// ============================================================ + +static void selectRange(WidgetT *w, int32_t from, int32_t to) { + if (!w->as.listBox.selBits) { + return; + } + + int32_t lo = from < to ? from : to; + int32_t hi = from > to ? from : to; + + if (lo < 0) { + lo = 0; + } + + if (hi >= w->as.listBox.itemCount) { + hi = w->as.listBox.itemCount - 1; + } + + for (int32_t i = lo; i <= hi; i++) { + w->as.listBox.selBits[i] = 1; + } +} + + +// ============================================================ +// widgetListBoxDestroy +// ============================================================ + +void widgetListBoxDestroy(WidgetT *w) { + if (w->as.listBox.selBits) { + free(w->as.listBox.selBits); + w->as.listBox.selBits = NULL; + } +} + + // ============================================================ // wgtListBox // ============================================================ @@ -17,12 +111,26 @@ WidgetT *wgtListBox(WidgetT *parent) { if (w) { w->as.listBox.selectedIdx = -1; + w->as.listBox.anchorIdx = -1; } return w; } +// ============================================================ +// wgtListBoxClearSelection +// ============================================================ + +void wgtListBoxClearSelection(WidgetT *w) { + if (!w || w->type != WidgetListBoxE || !w->as.listBox.selBits) { + return; + } + + memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount); +} + + // ============================================================ // wgtListBoxGetSelected // ============================================================ @@ -36,6 +144,57 @@ int32_t wgtListBoxGetSelected(const WidgetT *w) { } +// ============================================================ +// wgtListBoxIsItemSelected +// ============================================================ + +bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx) { + if (!w || w->type != WidgetListBoxE) { + return false; + } + + if (!w->as.listBox.multiSelect) { + return idx == w->as.listBox.selectedIdx; + } + + if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) { + return false; + } + + return w->as.listBox.selBits[idx] != 0; +} + + +// ============================================================ +// wgtListBoxSelectAll +// ============================================================ + +void wgtListBoxSelectAll(WidgetT *w) { + if (!w || w->type != WidgetListBoxE || !w->as.listBox.selBits) { + return; + } + + memset(w->as.listBox.selBits, 1, w->as.listBox.itemCount); +} + + +// ============================================================ +// wgtListBoxSetItemSelected +// ============================================================ + +void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) { + if (!w || w->type != WidgetListBoxE) { + return; + } + + if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) { + return; + } + + w->as.listBox.selBits[idx] = selected ? 1 : 0; +} + + // ============================================================ // wgtListBoxSetItems // ============================================================ @@ -68,6 +227,35 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { if (w->as.listBox.selectedIdx < 0 && count > 0) { w->as.listBox.selectedIdx = 0; } + + w->as.listBox.anchorIdx = w->as.listBox.selectedIdx; + + // Reallocate selection bits + allocSelBits(w); + + // Pre-select the cursor item + if (w->as.listBox.selBits && w->as.listBox.selectedIdx >= 0) { + w->as.listBox.selBits[w->as.listBox.selectedIdx] = 1; + } +} + + +// ============================================================ +// wgtListBoxSetMultiSelect +// ============================================================ + +void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) { + if (!w || w->type != WidgetListBoxE) { + return; + } + + w->as.listBox.multiSelect = multi; + allocSelBits(w); + + // Sync: mark current selection + if (w->as.listBox.selBits && w->as.listBox.selectedIdx >= 0) { + w->as.listBox.selBits[w->as.listBox.selectedIdx] = 1; + } } @@ -81,6 +269,16 @@ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) { } w->as.listBox.selectedIdx = idx; + w->as.listBox.anchorIdx = idx; + + // In multi-select, clear all then select just this one + if (w->as.listBox.selBits) { + memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount); + + if (idx >= 0 && idx < w->as.listBox.itemCount) { + w->as.listBox.selBits[idx] = 1; + } + } } @@ -106,52 +304,111 @@ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { - (void)mod; - if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) { return; } - int32_t sel = w->as.listBox.selectedIdx; + bool multi = w->as.listBox.multiSelect; + bool shift = (mod & KEY_MOD_SHIFT) != 0; + bool ctrl = (mod & KEY_MOD_CTRL) != 0; + int32_t sel = w->as.listBox.selectedIdx; + + // Ctrl+A — select all (multi-select only) + if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) { + wgtListBoxSelectAll(w); + wgtInvalidatePaint(w); + return; + } + + // Space — toggle current item (multi-select only) + if (multi && key == ' ') { + if (sel >= 0 && w->as.listBox.selBits) { + w->as.listBox.selBits[sel] ^= 1; + w->as.listBox.anchorIdx = sel; + } + + if (w->onChange) { + w->onChange(w); + } + + wgtInvalidatePaint(w); + return; + } + + int32_t newSel = sel; if (key == (0x50 | 0x100)) { // Down arrow - if (sel < w->as.listBox.itemCount - 1) { - w->as.listBox.selectedIdx = sel + 1; - } else if (sel < 0) { - w->as.listBox.selectedIdx = 0; + if (newSel < w->as.listBox.itemCount - 1) { + newSel++; + } else if (newSel < 0) { + newSel = 0; } } else if (key == (0x48 | 0x100)) { // Up arrow - if (sel > 0) { - w->as.listBox.selectedIdx = sel - 1; - } else if (sel < 0) { - w->as.listBox.selectedIdx = 0; + if (newSel > 0) { + newSel--; + } else if (newSel < 0) { + newSel = 0; } } else if (key == (0x47 | 0x100)) { // Home - w->as.listBox.selectedIdx = 0; + newSel = 0; } else if (key == (0x4F | 0x100)) { // End - w->as.listBox.selectedIdx = w->as.listBox.itemCount - 1; + newSel = w->as.listBox.itemCount - 1; + } else if (key == (0x51 | 0x100)) { + // Page Down + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + const BitmapFontT *font = &ctx->font; + int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; + + if (visibleRows < 1) { + visibleRows = 1; + } + + newSel += visibleRows; + + if (newSel >= w->as.listBox.itemCount) { + newSel = w->as.listBox.itemCount - 1; + } + } else if (key == (0x49 | 0x100)) { + // Page Up + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + const BitmapFontT *font = &ctx->font; + int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; + + if (visibleRows < 1) { + visibleRows = 1; + } + + newSel -= visibleRows; + + if (newSel < 0) { + newSel = 0; + } } else { return; } - // Scroll to keep selection visible - if (w->as.listBox.selectedIdx >= 0) { - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - int32_t innerH = w->h - LISTBOX_BORDER * 2; - int32_t visibleRows = innerH / font->charHeight; - - if (w->as.listBox.selectedIdx < w->as.listBox.scrollPos) { - w->as.listBox.scrollPos = w->as.listBox.selectedIdx; - } else if (w->as.listBox.selectedIdx >= w->as.listBox.scrollPos + visibleRows) { - w->as.listBox.scrollPos = w->as.listBox.selectedIdx - visibleRows + 1; - } + if (newSel == sel) { + return; } + w->as.listBox.selectedIdx = newSel; + + // Update selection + if (multi && w->as.listBox.selBits) { + if (shift) { + // Shift+arrow: range from anchor to new cursor + memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount); + selectRange(w, w->as.listBox.anchorIdx, newSel); + } + // Plain arrow: just move cursor, leave selections untouched + } + + ensureScrollVisible(w, newSel); + if (w->onChange) { w->onChange(w); } @@ -165,7 +422,7 @@ void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { // ============================================================ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { - AppContextT *ctx = (AppContextT *)root->userData; + AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t innerH = hit->h - LISTBOX_BORDER * 2; @@ -234,14 +491,37 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { int32_t clickedRow = relY / font->charHeight; int32_t idx = hit->as.listBox.scrollPos + clickedRow; - if (idx >= 0 && idx < hit->as.listBox.itemCount) { - hit->as.listBox.selectedIdx = idx; - hit->focused = true; + if (idx < 0 || idx >= hit->as.listBox.itemCount) { + return; + } - if (hit->onChange) { - hit->onChange(hit); + bool multi = hit->as.listBox.multiSelect; + bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0; + bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0; + + hit->as.listBox.selectedIdx = idx; + hit->focused = true; + + if (multi && hit->as.listBox.selBits) { + if (ctrl) { + // Ctrl+click: toggle item, update anchor + hit->as.listBox.selBits[idx] ^= 1; + hit->as.listBox.anchorIdx = idx; + } else if (shift) { + // Shift+click: range from anchor to clicked + memset(hit->as.listBox.selBits, 0, hit->as.listBox.itemCount); + selectRange(hit, hit->as.listBox.anchorIdx, idx); + } else { + // Plain click: select only this item, update anchor + memset(hit->as.listBox.selBits, 0, hit->as.listBox.itemCount); + hit->as.listBox.selBits[idx] = 1; + hit->as.listBox.anchorIdx = idx; } } + + if (hit->onChange) { + hit->onChange(hit); + } } @@ -276,9 +556,11 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm w->as.listBox.scrollPos = clampInt(w->as.listBox.scrollPos, 0, maxScroll); // Draw items - int32_t innerX = w->x + LISTBOX_BORDER + LISTBOX_PAD; - int32_t innerY = w->y + LISTBOX_BORDER; - int32_t scrollPos = w->as.listBox.scrollPos; + int32_t innerX = w->x + LISTBOX_BORDER + LISTBOX_PAD; + int32_t innerY = w->y + LISTBOX_BORDER; + int32_t scrollPos = w->as.listBox.scrollPos; + bool multi = w->as.listBox.multiSelect; + uint8_t *selBits = w->as.listBox.selBits; for (int32_t i = 0; i < visibleRows && (scrollPos + i) < w->as.listBox.itemCount; i++) { int32_t idx = scrollPos + i; @@ -286,13 +568,26 @@ void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm uint32_t ifg = fg; uint32_t ibg = bg; - if (idx == w->as.listBox.selectedIdx) { + bool isSelected; + + if (multi && selBits) { + isSelected = selBits[idx] != 0; + } else { + isSelected = (idx == w->as.listBox.selectedIdx); + } + + if (isSelected) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; rectFill(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, ibg); } drawText(d, ops, font, innerX, iy, w->as.listBox.items[idx], ifg, ibg, false); + + // Draw cursor indicator in multi-select (dotted focus rect on cursor item) + if (multi && idx == w->as.listBox.selectedIdx && w->focused) { + drawFocusRect(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, fg); + } } // Draw scrollbar diff --git a/dvx/widgets/widgetListView.c b/dvx/widgets/widgetListView.c index c533b66..2625b57 100644 --- a/dvx/widgets/widgetListView.c +++ b/dvx/widgets/widgetListView.c @@ -17,12 +17,31 @@ // 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 // ============================================================ @@ -310,6 +329,11 @@ void widgetListViewDestroy(WidgetT *w) { 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; + } } @@ -322,6 +346,7 @@ WidgetT *wgtListView(WidgetT *parent) { if (w) { w->as.listView.selectedIdx = -1; + w->as.listView.anchorIdx = -1; w->as.listView.sortCol = -1; w->as.listView.sortDir = ListViewSortNoneE; w->weight = 100; @@ -344,6 +369,53 @@ int32_t wgtListViewGetSelected(const WidgetT *w) { } +// ============================================================ +// 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 // ============================================================ @@ -390,8 +462,17 @@ void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount) { 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; + } } @@ -408,6 +489,41 @@ void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_ } +// ============================================================ +// 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; +} + + +// ============================================================ +// 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 // ============================================================ @@ -418,6 +534,15 @@ void wgtListViewSetSelected(WidgetT *w, int32_t idx) { } 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; + } + } } @@ -454,15 +579,40 @@ void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { - (void)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; @@ -529,7 +679,54 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod) { } // Convert display row back to data row - w->as.listView.selectedIdx = sortIdx ? sortIdx[displaySel] : displaySel; + 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) { @@ -770,6 +967,56 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) 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); } @@ -929,6 +1176,9 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // 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; @@ -936,7 +1186,15 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit uint32_t ifg = fg; uint32_t ibg = bg; - if (dataRow == w->as.listView.selectedIdx) { + 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); @@ -980,6 +1238,11 @@ void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit 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); + } } setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index 7f7c2f4..2780d93 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -471,6 +471,7 @@ static void setupControlsWindow(AppContextT *ctx) { wgtListViewSetColumns(lv, lvCols, 4); wgtListViewSetData(lv, lvData, 10); wgtListViewSetSelected(lv, 0); + wgtListViewSetMultiSelect(lv, true); lv->weight = 100; // --- Tab 4: Toolbar (ImageButtons + VSeparator) --- @@ -795,13 +796,19 @@ static void setupWidgetDemo(AppContextT *ctx) { wgtHSeparator(root); - // List box + // List boxes static const char *listItems[] = {"Alpha", "Beta", "Gamma", "Delta", "Epsilon"}; + static const char *multiItems[] = {"Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet"}; WidgetT *listRow = wgtHBox(root); wgtLabel(listRow, "&Items:"); WidgetT *lb = wgtListBox(listRow); wgtListBoxSetItems(lb, listItems, 5); lb->weight = 100; + wgtLabel(listRow, "&Multi:"); + WidgetT *mlb = wgtListBox(listRow); + wgtListBoxSetMultiSelect(mlb, true); + wgtListBoxSetItems(mlb, multiItems, 7); + mlb->weight = 100; wgtHSeparator(root);