Multi-select list boxes. Mouse selection when scrolled error fixed.

This commit is contained in:
Scott Duensing 2026-03-15 21:39:00 -05:00
parent 7476367ace
commit d525798836
9 changed files with 637 additions and 69 deletions

View file

@ -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

View file

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

View file

@ -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

View file

@ -131,7 +131,7 @@ static const WidgetClassT sClassListBox = {
.layout = NULL,
.onMouse = widgetListBoxOnMouse,
.onKey = widgetListBoxOnKey,
.destroy = NULL,
.destroy = widgetListBoxDestroy,
.getText = NULL,
.setText = NULL
};

View file

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

View file

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

View file

@ -1,13 +1,107 @@
// widgetListBox.c — ListBox widget
// widgetListBox.c — ListBox widget (single and multi-select)
#include "widgetInternal.h"
#include <stdlib.h>
#include <string.h>
#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

View file

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

View file

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