972 lines
29 KiB
C
972 lines
29 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetListBox.c -- ListBox widget (single and multi-select)
|
|
//
|
|
// Scrollable list of text items with single-select or multi-select modes.
|
|
// Items are stored as external string pointers (not copied), with a vertical
|
|
// scrollbar appearing when the item count exceeds visible rows.
|
|
//
|
|
// Multi-select uses a parallel selBits array (one byte per item, 0 or 1)
|
|
// rather than a bitfield. Using a full byte per item wastes some memory but
|
|
// makes individual item toggle/test trivial without shift/mask operations,
|
|
// which matters when the selection code runs on every click and keyboard event.
|
|
//
|
|
// The selection model follows Windows explorer conventions:
|
|
// - Plain click: select one item, clear others, set anchor
|
|
// - Ctrl+click: toggle one item, update anchor
|
|
// - Shift+click: range select from anchor to clicked item
|
|
// - Shift+arrow: range select from anchor to cursor
|
|
// - Space: toggle current item (multi-select)
|
|
// - Ctrl+A: select all (multi-select)
|
|
//
|
|
// The "anchor" concept is key: it's the starting point for shift-select
|
|
// ranges. It's updated on non-shift clicks but stays fixed during shift
|
|
// operations, allowing the user to extend/shrink the selection by
|
|
// shift-clicking different endpoints.
|
|
//
|
|
// Drag-reorder support allows items to be rearranged by dragging. When
|
|
// enabled, a mouse-down initiates a drag (tracked via sDragReorder global),
|
|
// and a 2px horizontal line indicator shows the insertion point. The actual
|
|
// reordering is handled by the application's onReorder callback.
|
|
|
|
#include "dvxWgtP.h"
|
|
#include "../listhelp/listHelp.h"
|
|
|
|
#define LISTBOX_BORDER 2
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
typedef struct {
|
|
const char **items;
|
|
int32_t itemCount;
|
|
int32_t selectedIdx;
|
|
int32_t scrollPos;
|
|
int32_t maxItemLen;
|
|
bool multiSelect;
|
|
int32_t anchorIdx;
|
|
uint8_t *selBits;
|
|
bool reorderable;
|
|
int32_t dragIdx;
|
|
int32_t dropIdx;
|
|
int32_t sbDragOrient;
|
|
int32_t sbDragOff;
|
|
// Owned item storage for AddItem/RemoveItem/Clear
|
|
char **ownedItems; // stb_ds dynamic array of strdup'd strings
|
|
bool ownsItems; // true if items[] points to ownedItems
|
|
} ListBoxDataT;
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "stb_ds_wrap.h"
|
|
|
|
#define LISTBOX_PAD 2
|
|
#define LISTBOX_MIN_ROWS 4
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void ensureScrollVisible(WidgetT *w, int32_t idx);
|
|
static void selectRange(WidgetT *w, int32_t from, int32_t to);
|
|
static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
|
|
void wgtListBoxSelectAll(WidgetT *w);
|
|
|
|
|
|
// ============================================================
|
|
// ensureScrollVisible
|
|
// ============================================================
|
|
|
|
// Adjust scroll position so the item at idx is within the visible viewport.
|
|
// If the item is above the viewport, scroll up to it. If below, scroll down
|
|
// to show it at the bottom. If already visible, do nothing.
|
|
static void ensureScrollVisible(WidgetT *w, int32_t idx) {
|
|
if (idx < 0) {
|
|
return;
|
|
}
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
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 < d->scrollPos) {
|
|
d->scrollPos = idx;
|
|
} else if (idx >= d->scrollPos + visibleRows) {
|
|
d->scrollPos = idx - visibleRows + 1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// selectRange
|
|
// ============================================================
|
|
|
|
// Set selection bits for all items in the range [from, to] (inclusive,
|
|
// order-independent). Used for shift-click and shift-arrow range selection.
|
|
static void selectRange(WidgetT *w, int32_t from, int32_t to) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (!d->selBits) {
|
|
return;
|
|
}
|
|
|
|
int32_t lo = from < to ? from : to;
|
|
int32_t hi = from > to ? from : to;
|
|
|
|
if (lo < 0) {
|
|
lo = 0;
|
|
}
|
|
|
|
if (hi >= d->itemCount) {
|
|
hi = d->itemCount - 1;
|
|
}
|
|
|
|
for (int32_t i = lo; i <= hi; i++) {
|
|
d->selBits[i] = 1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxDestroy
|
|
// ============================================================
|
|
|
|
void widgetListBoxDestroy(WidgetT *w) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (d) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
|
|
free(d->ownedItems[i]);
|
|
}
|
|
arrfree(d->ownedItems);
|
|
free(d->selBits);
|
|
free(d);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxCalcMinSize
|
|
// ============================================================
|
|
|
|
// Min size accounts for the widest item, a scrollbar, padding, and border.
|
|
// Height is based on LISTBOX_MIN_ROWS (4 rows) so the listbox has a usable
|
|
// minimum height even when empty. The 8-character minimum width prevents
|
|
// the listbox from collapsing too narrow when items are short or absent.
|
|
void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
int32_t maxItemW = d->maxItemLen * font->charWidth;
|
|
int32_t minW = font->charWidth * 8;
|
|
|
|
if (maxItemW < minW) {
|
|
maxItemW = minW;
|
|
}
|
|
|
|
w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + WGT_SB_W;
|
|
w->calcMinH = LISTBOX_MIN_ROWS * font->charHeight + LISTBOX_BORDER * 2;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxOnKey
|
|
// ============================================================
|
|
|
|
// Key handling: delegates to widgetNavigateIndex for cursor movement
|
|
// (Up, Down, Home, End, PgUp, PgDn) which returns the new index after
|
|
// navigation. This shared helper ensures consistent keyboard navigation
|
|
// across ListBox, ListView, and other scrollable widgets.
|
|
void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (!w || w->type != sTypeId || d->itemCount == 0) {
|
|
return;
|
|
}
|
|
|
|
bool multi = d->multiSelect;
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (mod & KEY_MOD_CTRL) != 0;
|
|
int32_t sel = d->selectedIdx;
|
|
|
|
// Ctrl+A -- select all (multi-select only)
|
|
if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) {
|
|
wgtListBoxSelectAll(w);
|
|
return;
|
|
}
|
|
|
|
// Space -- toggle current item (multi-select only)
|
|
if (multi && key == ' ') {
|
|
if (sel >= 0 && d->selBits) {
|
|
d->selBits[sel] ^= 1;
|
|
d->anchorIdx = sel;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Enter -- activate selected item (same as double-click)
|
|
if (key == '\r' || key == '\n') {
|
|
if (w->onDblClick && sel >= 0) {
|
|
w->onDblClick(w);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight;
|
|
|
|
if (visibleRows < 1) {
|
|
visibleRows = 1;
|
|
}
|
|
|
|
int32_t newSel = widgetNavigateIndex(key, sel, d->itemCount, visibleRows);
|
|
|
|
if (newSel < 0) {
|
|
return;
|
|
}
|
|
|
|
if (newSel == sel) {
|
|
return;
|
|
}
|
|
|
|
d->selectedIdx = newSel;
|
|
d->dragIdx = -1;
|
|
d->dropIdx = -1;
|
|
|
|
// Update selection
|
|
if (multi && d->selBits) {
|
|
if (shift) {
|
|
// Shift+arrow: range from anchor to new cursor
|
|
memset(d->selBits, 0, d->itemCount);
|
|
selectRange(w, d->anchorIdx, newSel);
|
|
}
|
|
// Plain arrow: just move cursor, leave selections untouched
|
|
}
|
|
|
|
ensureScrollVisible(w, newSel);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxOnMouse
|
|
// ============================================================
|
|
|
|
// Mouse handling: first checks if the click is on the scrollbar (right edge),
|
|
// then falls through to item click handling. The scrollbar hit-test uses
|
|
// widgetScrollbarHitTest which divides the scrollbar into arrow buttons, page
|
|
// regions, and thumb based on the click position. Item clicks are translated
|
|
// from pixel coordinates to item index using integer division by charHeight.
|
|
void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
ListBoxDataT *d = (ListBoxDataT *)hit->data;
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
const BitmapFontT *font = &ctx->font;
|
|
|
|
int32_t innerH = hit->h - LISTBOX_BORDER * 2;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
bool needSb = (d->itemCount > visibleRows);
|
|
|
|
// Clamp scroll
|
|
int32_t maxScroll = d->itemCount - visibleRows;
|
|
|
|
if (maxScroll < 0) {
|
|
maxScroll = 0;
|
|
}
|
|
|
|
d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
|
|
|
|
// Check if click is on the scrollbar
|
|
if (needSb) {
|
|
int32_t sbX = hit->x + hit->w - LISTBOX_BORDER - WGT_SB_W;
|
|
|
|
if (vx >= sbX) {
|
|
int32_t relY = vy - (hit->y + LISTBOX_BORDER);
|
|
ScrollHitE sh = widgetScrollbarHitTest(innerH, relY, d->itemCount, visibleRows, d->scrollPos);
|
|
|
|
if (sh == ScrollHitArrowDecE) {
|
|
if (d->scrollPos > 0) {
|
|
d->scrollPos--;
|
|
}
|
|
} else if (sh == ScrollHitArrowIncE) {
|
|
if (d->scrollPos < maxScroll) {
|
|
d->scrollPos++;
|
|
}
|
|
} else if (sh == ScrollHitPageDecE) {
|
|
d->scrollPos -= visibleRows;
|
|
d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
|
|
} else if (sh == ScrollHitPageIncE) {
|
|
d->scrollPos += visibleRows;
|
|
d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
|
|
} else if (sh == ScrollHitThumbE) {
|
|
int32_t trackLen = innerH - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, d->itemCount, visibleRows, d->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
sDragWidget = hit;
|
|
d->sbDragOrient = 0;
|
|
d->sbDragOff = relY - WGT_SB_W - thumbPos;
|
|
}
|
|
|
|
sFocusedWidget = hit;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Click on item area
|
|
int32_t innerY = hit->y + LISTBOX_BORDER;
|
|
int32_t relY = vy - innerY;
|
|
|
|
if (relY < 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t clickedRow = relY / font->charHeight;
|
|
int32_t idx = d->scrollPos + clickedRow;
|
|
|
|
if (idx < 0 || idx >= d->itemCount) {
|
|
return;
|
|
}
|
|
|
|
bool multi = d->multiSelect;
|
|
bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0;
|
|
bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0;
|
|
|
|
d->selectedIdx = idx;
|
|
sFocusedWidget = hit;
|
|
|
|
if (multi && d->selBits) {
|
|
if (ctrl) {
|
|
// Ctrl+click: toggle item, update anchor
|
|
d->selBits[idx] ^= 1;
|
|
d->anchorIdx = idx;
|
|
} else if (shift) {
|
|
// Shift+click: range from anchor to clicked
|
|
memset(d->selBits, 0, d->itemCount);
|
|
selectRange(hit, d->anchorIdx, idx);
|
|
} else {
|
|
// Plain click: select only this item, update anchor
|
|
memset(d->selBits, 0, d->itemCount);
|
|
d->selBits[idx] = 1;
|
|
d->anchorIdx = idx;
|
|
}
|
|
}
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
|
|
// onDblClick is handled by the central dispatcher in widgetEvent.c
|
|
|
|
// Initiate drag-reorder if enabled (not from modifier clicks)
|
|
if (d->reorderable && !shift && !ctrl) {
|
|
d->dragIdx = idx;
|
|
d->dropIdx = idx;
|
|
sDragWidget = hit;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxPaint
|
|
// ============================================================
|
|
|
|
// Paint: sunken bevel border, then iterate visible rows drawing text.
|
|
// Selected items get highlight background (full row width) with contrasting
|
|
// text color. In multi-select mode, the cursor item (selectedIdx) gets a
|
|
// dotted focus rect overlay -- this is separate from the selection highlight
|
|
// so the user can see which item the keyboard cursor is on even when multiple
|
|
// items are selected. The scrollbar is drawn only when needed (item count
|
|
// exceeds visible rows), and the content width is reduced accordingly.
|
|
void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
ListBoxDataT *lb = (ListBoxDataT *)w->data;
|
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
|
|
|
int32_t innerH = w->h - LISTBOX_BORDER * 2;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
bool needSb = (lb->itemCount > visibleRows);
|
|
int32_t contentW = w->w - LISTBOX_BORDER * 2;
|
|
|
|
if (needSb) {
|
|
contentW -= WGT_SB_W;
|
|
}
|
|
|
|
// Sunken border
|
|
BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2);
|
|
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
|
|
|
// Clamp scroll position
|
|
int32_t maxScroll = lb->itemCount - visibleRows;
|
|
|
|
if (maxScroll < 0) {
|
|
maxScroll = 0;
|
|
}
|
|
|
|
lb->scrollPos = clampInt(lb->scrollPos, 0, maxScroll);
|
|
|
|
// Draw items
|
|
int32_t innerX = w->x + LISTBOX_BORDER + LISTBOX_PAD;
|
|
int32_t innerY = w->y + LISTBOX_BORDER;
|
|
int32_t scrollPos = lb->scrollPos;
|
|
bool multi = lb->multiSelect;
|
|
uint8_t *selBits = lb->selBits;
|
|
|
|
for (int32_t i = 0; i < visibleRows && (scrollPos + i) < lb->itemCount; i++) {
|
|
int32_t idx = scrollPos + i;
|
|
int32_t iy = innerY + i * font->charHeight;
|
|
uint32_t ifg = fg;
|
|
uint32_t ibg = bg;
|
|
|
|
bool isSelected;
|
|
|
|
if (multi && selBits) {
|
|
isSelected = selBits[idx] != 0;
|
|
} else {
|
|
isSelected = (idx == lb->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, lb->items[idx], ifg, ibg, false);
|
|
|
|
// Draw cursor indicator in multi-select (dotted focus rect on cursor item)
|
|
if (multi && idx == lb->selectedIdx && w == sFocusedWidget) {
|
|
drawFocusRect(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, fg);
|
|
}
|
|
}
|
|
|
|
// Draw drag-reorder insertion indicator: a 2px horizontal line at the
|
|
// drop position. The line is drawn between items (not on top of them)
|
|
// so it's clear where the dragged item will be inserted.
|
|
if (lb->reorderable && lb->dragIdx >= 0 && lb->dropIdx >= 0) {
|
|
int32_t drop = lb->dropIdx;
|
|
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
|
|
|
|
if (lineY >= innerY && lineY <= innerY + innerH) {
|
|
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY, contentW, colors->contentFg);
|
|
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY + 1, contentW, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
// Draw scrollbar
|
|
if (needSb) {
|
|
int32_t sbX = w->x + w->w - LISTBOX_BORDER - WGT_SB_W;
|
|
int32_t sbY = w->y + LISTBOX_BORDER;
|
|
widgetDrawScrollbarV(d, ops, colors, sbX, sbY, innerH, lb->itemCount, visibleRows, lb->scrollPos);
|
|
}
|
|
|
|
if (w == sFocusedWidget) {
|
|
drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxScrollDragUpdate
|
|
// ============================================================
|
|
|
|
// Handle scrollbar thumb drag for vertical scrollbar.
|
|
static void widgetListBoxScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
|
|
(void)mouseX;
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
|
|
if (orient != 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t innerH = w->h - LISTBOX_BORDER * 2;
|
|
int32_t visibleRows = innerH / font->charHeight;
|
|
int32_t maxScroll = d->itemCount - visibleRows;
|
|
|
|
if (maxScroll <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t trackLen = innerH - WGT_SB_W * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, d->itemCount, visibleRows, d->scrollPos, &thumbPos, &thumbSize);
|
|
|
|
int32_t sbY = w->y + LISTBOX_BORDER;
|
|
int32_t relMouse = mouseY - sbY - WGT_SB_W - dragOff;
|
|
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;
|
|
d->scrollPos = clampInt(newScroll, 0, maxScroll);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static void widgetListBoxReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
(void)root;
|
|
(void)x;
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t innerY = w->y + LISTBOX_BORDER;
|
|
int32_t relY = y - innerY;
|
|
int32_t dropIdx = d->scrollPos + relY / font->charHeight;
|
|
|
|
if (dropIdx < 0) {
|
|
dropIdx = 0;
|
|
}
|
|
|
|
if (dropIdx > d->itemCount) {
|
|
dropIdx = d->itemCount;
|
|
}
|
|
|
|
d->dropIdx = dropIdx;
|
|
|
|
// Auto-scroll when dragging near edges
|
|
int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight;
|
|
|
|
if (relY < font->charHeight && d->scrollPos > 0) {
|
|
d->scrollPos--;
|
|
} else if (relY >= (visibleRows - 1) * font->charHeight && d->scrollPos < d->itemCount - visibleRows) {
|
|
d->scrollPos++;
|
|
}
|
|
}
|
|
|
|
|
|
static void widgetListBoxReorderDrop(WidgetT *w) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
int32_t from = d->dragIdx;
|
|
int32_t to = d->dropIdx;
|
|
|
|
d->dragIdx = -1;
|
|
d->dropIdx = -1;
|
|
|
|
if (from < 0 || to < 0 || from == to || from == to - 1) {
|
|
return;
|
|
}
|
|
|
|
if (from < 0 || from >= d->itemCount) {
|
|
return;
|
|
}
|
|
|
|
// Move the item by shifting the pointer array
|
|
const char *moving = d->items[from];
|
|
|
|
if (to > from) {
|
|
// Moving down: shift items up
|
|
for (int32_t i = from; i < to - 1; i++) {
|
|
d->items[i] = d->items[i + 1];
|
|
}
|
|
|
|
d->items[to - 1] = moving;
|
|
d->selectedIdx = to - 1;
|
|
} else {
|
|
// Moving up: shift items down
|
|
for (int32_t i = from; i > to; i--) {
|
|
d->items[i] = d->items[i - 1];
|
|
}
|
|
|
|
d->items[to] = moving;
|
|
d->selectedIdx = to;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxOnDragEnd
|
|
// ============================================================
|
|
|
|
static void widgetListBoxOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
(void)root;
|
|
(void)x;
|
|
(void)y;
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (d->dragIdx >= 0) {
|
|
widgetListBoxReorderDrop(w);
|
|
d->dragIdx = -1;
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetListBoxOnDragUpdate
|
|
// ============================================================
|
|
|
|
static void widgetListBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (d->dragIdx >= 0) {
|
|
widgetListBoxReorderUpdate(w, root, x, y);
|
|
} else {
|
|
widgetListBoxScrollDragUpdate(w, d->sbDragOrient, d->sbDragOff, x, y);
|
|
}
|
|
}
|
|
|
|
|
|
static const WidgetClassT sClassListBox = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetListBoxPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetListBoxCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetListBoxOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetListBoxOnKey,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetListBoxDestroy,
|
|
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetListBoxOnDragUpdate,
|
|
[WGT_METHOD_ON_DRAG_END] = (void *)widgetListBoxOnDragEnd,
|
|
}
|
|
};
|
|
|
|
// ============================================================
|
|
// Widget creation functions and accessors
|
|
// ============================================================
|
|
|
|
|
|
// Allocate (or re-allocate) the selection bits array for multi-select mode.
|
|
static void listBoxAllocSelBits(WidgetT *w) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
free(d->selBits);
|
|
d->selBits = NULL;
|
|
|
|
int32_t count = d->itemCount;
|
|
|
|
if (count > 0 && d->multiSelect) {
|
|
d->selBits = (uint8_t *)calloc(count, 1);
|
|
}
|
|
}
|
|
|
|
|
|
WidgetT *wgtListBox(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, sTypeId);
|
|
|
|
if (w) {
|
|
ListBoxDataT *d = (ListBoxDataT *)calloc(1, sizeof(ListBoxDataT));
|
|
|
|
if (!d) {
|
|
return w;
|
|
}
|
|
|
|
w->data = d;
|
|
d->selectedIdx = -1;
|
|
d->anchorIdx = -1;
|
|
d->dragIdx = -1;
|
|
d->dropIdx = -1;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
void wgtListBoxClearSelection(WidgetT *w) {
|
|
if (!w || w->type != sTypeId) {
|
|
return;
|
|
}
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (!d->selBits) {
|
|
return;
|
|
}
|
|
|
|
memset(d->selBits, 0, d->itemCount);
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
int32_t wgtListBoxGetSelected(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, -1);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
return d->selectedIdx;
|
|
}
|
|
|
|
|
|
bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET(w, sTypeId, false);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (!d->multiSelect) {
|
|
return idx == d->selectedIdx;
|
|
}
|
|
|
|
if (!d->selBits || idx < 0 || idx >= d->itemCount) {
|
|
return false;
|
|
}
|
|
|
|
return d->selBits[idx] != 0;
|
|
}
|
|
|
|
|
|
void wgtListBoxSelectAll(WidgetT *w) {
|
|
if (!w || w->type != sTypeId) {
|
|
return;
|
|
}
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (!d->selBits) {
|
|
return;
|
|
}
|
|
|
|
memset(d->selBits, 1, d->itemCount);
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
if (!d->selBits || idx < 0 || idx >= d->itemCount) {
|
|
return;
|
|
}
|
|
|
|
d->selBits[idx] = selected ? 1 : 0;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
d->items = items;
|
|
d->itemCount = count;
|
|
d->maxItemLen = widgetMaxItemLen(items, count);
|
|
|
|
if (d->selectedIdx >= count) {
|
|
d->selectedIdx = count > 0 ? 0 : -1;
|
|
}
|
|
|
|
if (d->selectedIdx < 0 && count > 0) {
|
|
d->selectedIdx = 0;
|
|
}
|
|
|
|
d->anchorIdx = d->selectedIdx;
|
|
|
|
// Reallocate selection bits
|
|
listBoxAllocSelBits(w);
|
|
|
|
// Pre-select the cursor item
|
|
if (d->selBits && d->selectedIdx >= 0) {
|
|
d->selBits[d->selectedIdx] = 1;
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
static void listBoxSyncOwned(WidgetT *w) {
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
d->items = (const char **)d->ownedItems;
|
|
d->itemCount = (int32_t)arrlen(d->ownedItems);
|
|
d->maxItemLen = widgetMaxItemLen(d->items, d->itemCount);
|
|
d->ownsItems = true;
|
|
listBoxAllocSelBits(w);
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtListBoxAddItem(WidgetT *w, const char *text) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
arrput(d->ownedItems, strdup(text ? text : ""));
|
|
listBoxSyncOwned(w);
|
|
}
|
|
|
|
|
|
void wgtListBoxRemoveItem(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
|
|
free(d->ownedItems[idx]);
|
|
arrdel(d->ownedItems, idx);
|
|
listBoxSyncOwned(w);
|
|
if (d->selectedIdx >= d->itemCount) {
|
|
d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
|
|
}
|
|
}
|
|
|
|
|
|
void wgtListBoxClear(WidgetT *w) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
|
|
free(d->ownedItems[i]);
|
|
}
|
|
arrsetlen(d->ownedItems, 0);
|
|
listBoxSyncOwned(w);
|
|
d->selectedIdx = -1;
|
|
d->scrollPos = 0;
|
|
}
|
|
|
|
|
|
const char *wgtListBoxGetItem(const WidgetT *w, int32_t idx) {
|
|
if (!w || w->type != sTypeId) { return ""; }
|
|
const ListBoxDataT *d = (const ListBoxDataT *)w->data;
|
|
if (idx < 0 || idx >= d->itemCount) { return ""; }
|
|
return d->items[idx] ? d->items[idx] : "";
|
|
}
|
|
|
|
|
|
int32_t wgtListBoxGetItemCount(const WidgetT *w) {
|
|
if (!w || w->type != sTypeId) { return 0; }
|
|
const ListBoxDataT *d = (const ListBoxDataT *)w->data;
|
|
return d->itemCount;
|
|
}
|
|
|
|
|
|
void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
d->multiSelect = multi;
|
|
listBoxAllocSelBits(w);
|
|
|
|
// Sync: mark current selection
|
|
if (d->selBits && d->selectedIdx >= 0) {
|
|
d->selBits[d->selectedIdx] = 1;
|
|
}
|
|
}
|
|
|
|
|
|
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
d->reorderable = reorderable;
|
|
}
|
|
|
|
|
|
void wgtListBoxSetSelected(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
|
|
ListBoxDataT *d = (ListBoxDataT *)w->data;
|
|
|
|
d->selectedIdx = idx;
|
|
d->anchorIdx = idx;
|
|
d->scrollPos = 0;
|
|
|
|
// In multi-select, clear all then select just this one
|
|
if (d->selBits) {
|
|
memset(d->selBits, 0, d->itemCount);
|
|
|
|
if (idx >= 0 && idx < d->itemCount) {
|
|
d->selBits[idx] = 1;
|
|
}
|
|
}
|
|
|
|
// Scroll to make the selected item visible
|
|
ensureScrollVisible(w, idx);
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent);
|
|
void (*setItems)(WidgetT *w, const char **items, int32_t count);
|
|
int32_t (*getSelected)(const WidgetT *w);
|
|
void (*setSelected)(WidgetT *w, int32_t idx);
|
|
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 (*removeItem)(WidgetT *w, int32_t idx);
|
|
void (*clear)(WidgetT *w);
|
|
const char *(*getItem)(const WidgetT *w, int32_t idx);
|
|
int32_t (*getItemCount)(const WidgetT *w);
|
|
} sApi = {
|
|
.create = wgtListBox,
|
|
.setItems = wgtListBoxSetItems,
|
|
.getSelected = wgtListBoxGetSelected,
|
|
.setSelected = wgtListBoxSetSelected,
|
|
.setMultiSelect = wgtListBoxSetMultiSelect,
|
|
.isItemSelected = wgtListBoxIsItemSelected,
|
|
.setItemSelected = wgtListBoxSetItemSelected,
|
|
.selectAll = wgtListBoxSelectAll,
|
|
.clearSelection = wgtListBoxClearSelection,
|
|
.setReorderable = wgtListBoxSetReorderable,
|
|
.addItem = wgtListBoxAddItem,
|
|
.removeItem = wgtListBoxRemoveItem,
|
|
.clear = wgtListBoxClear,
|
|
.getItem = wgtListBoxGetItem,
|
|
.getItemCount = wgtListBoxGetItemCount
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "ListIndex", WGT_IFACE_INT, (void *)wgtListBoxGetSelected, (void *)wgtListBoxSetSelected, NULL }
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "AddItem", WGT_SIG_STR, (void *)wgtListBoxAddItem },
|
|
{ "Clear", WGT_SIG_VOID, (void *)wgtListBoxClear },
|
|
{ "ClearSelection", WGT_SIG_VOID, (void *)wgtListBoxClearSelection },
|
|
{ "IsItemSelected", WGT_SIG_RET_BOOL_INT, (void *)wgtListBoxIsItemSelected },
|
|
{ "List", WGT_SIG_RET_STR_INT, (void *)wgtListBoxGetItem },
|
|
{ "ListCount", WGT_SIG_RET_INT, (void *)wgtListBoxGetItemCount },
|
|
{ "RemoveItem", WGT_SIG_INT, (void *)wgtListBoxRemoveItem },
|
|
{ "SelectAll", WGT_SIG_VOID, (void *)wgtListBoxSelectAll },
|
|
{ "SetItemSelected", WGT_SIG_INT_BOOL, (void *)wgtListBoxSetItemSelected },
|
|
{ "SetMultiSelect", WGT_SIG_BOOL, (void *)wgtListBoxSetMultiSelect },
|
|
{ "SetReorderable", WGT_SIG_BOOL, (void *)wgtListBoxSetReorderable },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "ListBox",
|
|
.props = sProps,
|
|
.propCount = 1,
|
|
.methods = sMethods,
|
|
.methodCount = 11,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassListBox);
|
|
wgtRegisterApi("listbox", &sApi);
|
|
wgtRegisterIface("listbox", &sIface);
|
|
}
|