549 lines
18 KiB
C
549 lines
18 KiB
C
#define DVX_WIDGET_IMPL
|
|
// widgetDropdown.c -- Dropdown (select) widget
|
|
//
|
|
// A non-editable dropdown list (HTML <select> equivalent). Unlike ComboBox,
|
|
// the user cannot type a custom value -- they can only choose from the
|
|
// predefined items. This simplifies the widget considerably: no text buffer,
|
|
// no undo, no cursor, no text editing key handling.
|
|
//
|
|
// The visual layout is identical to ComboBox (sunken display area + raised
|
|
// dropdown button), but the display area shows the selected item's text
|
|
// as read-only. A focus rect is drawn inside the display area when focused
|
|
// (since there's no cursor to indicate focus).
|
|
//
|
|
// Keyboard behavior when closed: Up arrow decrements selection immediately
|
|
// (inline cycling), Down/Space/Enter opens the popup. This matches the
|
|
// Win3.1 dropdown behavior where arrow keys cycle through values without
|
|
// opening the full list.
|
|
//
|
|
// Popup overlay painting and hit testing are shared with ComboBox via
|
|
// widgetDropdownPopupRect and widgetPaintPopupList in widgetCore.c.
|
|
|
|
#include "dvxWgtP.h"
|
|
#include "../texthelp/textHelp.h"
|
|
#include "../listhelp/listHelp.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "stb_ds_wrap.h"
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
typedef struct {
|
|
const char **items;
|
|
int32_t itemCount;
|
|
int32_t selectedIdx;
|
|
bool open;
|
|
int32_t hoverIdx;
|
|
int32_t scrollPos;
|
|
int32_t maxItemLen;
|
|
// 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
|
|
} DropdownDataT;
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownDestroy
|
|
// ============================================================
|
|
|
|
void widgetDropdownDestroy(WidgetT *w) {
|
|
DropdownDataT *d = (DropdownDataT *)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);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownGetText
|
|
// ============================================================
|
|
|
|
const char *widgetDropdownGetText(const WidgetT *w) {
|
|
const DropdownDataT *d = (const DropdownDataT *)w->data;
|
|
|
|
if (d->selectedIdx >= 0 && d->selectedIdx < d->itemCount) {
|
|
return d->items[d->selectedIdx];
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownCalcMinSize
|
|
// ============================================================
|
|
|
|
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
|
|
// Width: widest item + button width + border
|
|
int32_t maxItemW = d->maxItemLen * font->charWidth;
|
|
int32_t minW = font->charWidth * 8;
|
|
|
|
if (maxItemW < minW) {
|
|
maxItemW = minW;
|
|
}
|
|
|
|
w->calcMinW = maxItemW + DROPDOWN_BTN_WIDTH + TEXT_INPUT_PAD * 2 + 4;
|
|
w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownOnKey
|
|
// ============================================================
|
|
|
|
// Key handling: when popup is open, Up/Down navigate the hover index and
|
|
// Enter/Space confirms selection. When closed, Down/Space/Enter opens the
|
|
// popup, while Up decrements the selection inline (without opening). This
|
|
// two-mode behavior matches the standard Windows combo box UX where quick
|
|
// arrow-key cycling doesn't require the popup to be visible.
|
|
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
|
|
if (d->open) {
|
|
// Popup is open -- navigate items
|
|
if (key == (0x48 | 0x100)) {
|
|
if (d->hoverIdx > 0) {
|
|
d->hoverIdx--;
|
|
|
|
if (d->hoverIdx < d->scrollPos) {
|
|
d->scrollPos = d->hoverIdx;
|
|
}
|
|
}
|
|
} else if (key == (0x50 | 0x100)) {
|
|
if (d->hoverIdx < d->itemCount - 1) {
|
|
d->hoverIdx++;
|
|
|
|
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
|
|
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
|
}
|
|
}
|
|
} else if (key == 0x0D || key == ' ') {
|
|
d->selectedIdx = d->hoverIdx;
|
|
d->open = false;
|
|
sOpenPopup = NULL;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else {
|
|
// Popup is closed
|
|
if (key == ' ' || key == 0x0D) {
|
|
d->open = true;
|
|
d->hoverIdx = d->selectedIdx;
|
|
sOpenPopup = w;
|
|
|
|
if (d->hoverIdx >= d->scrollPos + DROPDOWN_MAX_VISIBLE) {
|
|
d->scrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
|
}
|
|
|
|
if (d->hoverIdx < d->scrollPos) {
|
|
d->scrollPos = d->hoverIdx;
|
|
}
|
|
} else if (key == (0x50 | 0x100)) {
|
|
// Down arrow: cycle selection forward (wheel-friendly)
|
|
if (d->selectedIdx < d->itemCount - 1) {
|
|
d->selectedIdx++;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else if (key == (0x48 | 0x100)) {
|
|
if (d->selectedIdx > 0) {
|
|
d->selectedIdx--;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownOnMouse
|
|
// ============================================================
|
|
|
|
// Mouse click toggles the popup open/closed. The sClosedPopup guard prevents
|
|
// a re-open race condition: when a click outside the popup closes it, the
|
|
// widget event dispatcher first closes the popup (setting sClosedPopup),
|
|
// then dispatches the click to whatever widget is under the cursor. If that
|
|
// happens to be this dropdown's body, we'd immediately re-open without this
|
|
// check. The sClosedPopup reference is cleared on the next event cycle.
|
|
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|
(void)root;
|
|
(void)vx;
|
|
sFocusedWidget = w;
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
|
|
// If popup is open, this click is on a popup item -- select it
|
|
if (d->open) {
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t popX;
|
|
int32_t popY;
|
|
int32_t popW;
|
|
int32_t popH;
|
|
|
|
widgetDropdownPopupRect(w, font, w->window->contentH, d->itemCount, &popX, &popY, &popW, &popH);
|
|
|
|
int32_t itemIdx = d->scrollPos + (vy - popY - 2) / font->charHeight;
|
|
|
|
if (itemIdx >= 0 && itemIdx < d->itemCount) {
|
|
d->selectedIdx = itemIdx;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
d->open = false;
|
|
sOpenPopup = NULL;
|
|
return;
|
|
}
|
|
|
|
// If this dropdown's popup was just closed by click-outside, don't re-open
|
|
if (w == sClosedPopup) {
|
|
return;
|
|
}
|
|
|
|
d->open = true;
|
|
d->hoverIdx = d->selectedIdx;
|
|
sOpenPopup = w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownPaint
|
|
// ============================================================
|
|
|
|
void widgetDropdownPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
|
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
|
|
|
// Sunken text area
|
|
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
|
|
BevelStyleT bevel;
|
|
bevel.highlight = colors->windowShadow;
|
|
bevel.shadow = colors->windowHighlight;
|
|
bevel.face = bg;
|
|
bevel.width = 2;
|
|
drawBevel(disp, ops, w->x, w->y, textAreaW, w->h, &bevel);
|
|
|
|
// Draw selected item text
|
|
if (d->selectedIdx >= 0 && d->selectedIdx < d->itemCount) {
|
|
int32_t textX = w->x + TEXT_INPUT_PAD;
|
|
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
|
|
|
if (!w->enabled) {
|
|
drawTextEmbossed(disp, ops, font, textX, textY, d->items[d->selectedIdx], colors);
|
|
} else {
|
|
drawText(disp, ops, font, textX, textY, d->items[d->selectedIdx], fg, bg, true);
|
|
}
|
|
}
|
|
|
|
// Drop button
|
|
BevelStyleT btnBevel;
|
|
btnBevel.highlight = d->open ? colors->windowShadow : colors->windowHighlight;
|
|
btnBevel.shadow = d->open ? colors->windowHighlight : colors->windowShadow;
|
|
btnBevel.face = colors->buttonFace;
|
|
btnBevel.width = 2;
|
|
drawBevel(disp, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel);
|
|
|
|
// Down arrow glyph in button
|
|
uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow;
|
|
int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2;
|
|
int32_t arrowY = w->y + w->h / 2 - 1;
|
|
widgetDrawDropdownArrow(disp, ops, arrowX, arrowY, arrowFg);
|
|
|
|
if (w == sFocusedWidget) {
|
|
drawFocusRect(disp, ops, w->x + 3, w->y + 3, textAreaW - 6, w->h - 6, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownPaintPopup
|
|
// ============================================================
|
|
|
|
// Paint the popup overlay list. Delegates to widgetDropdownPopupRect (which
|
|
// computes position, handling screen-edge flip to ensure the popup stays
|
|
// on-screen) and widgetPaintPopupList (which renders the bordered, scrollable
|
|
// item list with hover highlighting). These shared helpers are also used by
|
|
// ComboBox, keeping the popup rendering consistent between both widgets.
|
|
void widgetDropdownPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
int32_t popX;
|
|
int32_t popY;
|
|
int32_t popW;
|
|
int32_t popH;
|
|
|
|
widgetDropdownPopupRect(w, font, disp->clipH, d->itemCount, &popX, &popY, &popW, &popH);
|
|
widgetPaintPopupList(disp, ops, font, colors, popX, popY, popW, popH, d->items, d->itemCount, d->hoverIdx, d->scrollPos);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownAccelActivate
|
|
// ============================================================
|
|
|
|
void widgetDropdownAccelActivate(WidgetT *w, WidgetT *root) {
|
|
(void)root;
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
d->open = true;
|
|
d->hoverIdx = d->selectedIdx;
|
|
sOpenPopup = w;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownClosePopup
|
|
// ============================================================
|
|
|
|
void widgetDropdownClosePopup(WidgetT *w) {
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
d->open = false;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static void widgetDropdownGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
|
|
const DropdownDataT *d = (const DropdownDataT *)w->data;
|
|
widgetDropdownPopupRect((WidgetT *)w, font, contentH, d->itemCount, popX, popY, popW, popH);
|
|
}
|
|
|
|
|
|
static const WidgetClassT sClassDropdown = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetDropdownPaint,
|
|
[WGT_METHOD_PAINT_OVERLAY] = (void *)widgetDropdownPaintPopup,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetDropdownCalcMinSize,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetDropdownOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetDropdownOnKey,
|
|
[WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetDropdownAccelActivate,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetDropdownDestroy,
|
|
[WGT_METHOD_GET_TEXT] = (void *)widgetDropdownGetText,
|
|
[WGT_METHOD_CLOSE_POPUP] = (void *)widgetDropdownClosePopup,
|
|
[WGT_METHOD_GET_POPUP_RECT] = (void *)widgetDropdownGetPopupRect,
|
|
}
|
|
};
|
|
|
|
// ============================================================
|
|
// Widget creation functions
|
|
// ============================================================
|
|
|
|
|
|
WidgetT *wgtDropdown(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, sTypeId);
|
|
|
|
if (w) {
|
|
DropdownDataT *d = (DropdownDataT *)calloc(1, sizeof(DropdownDataT));
|
|
|
|
if (!d) {
|
|
return w;
|
|
}
|
|
|
|
w->data = d;
|
|
d->selectedIdx = -1;
|
|
d->hoverIdx = -1;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
int32_t wgtDropdownGetSelected(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTypeId, -1);
|
|
const DropdownDataT *d = (const DropdownDataT *)w->data;
|
|
|
|
return d->selectedIdx;
|
|
}
|
|
|
|
|
|
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
|
|
d->items = items;
|
|
d->itemCount = count;
|
|
d->maxItemLen = widgetMaxItemLen(items, count);
|
|
|
|
if (d->selectedIdx >= count) {
|
|
d->selectedIdx = -1;
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
|
|
d->selectedIdx = idx;
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// dropdownSyncOwned
|
|
// ============================================================
|
|
|
|
static void dropdownSyncOwned(WidgetT *w) {
|
|
DropdownDataT *d = (DropdownDataT *)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;
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownAddItem
|
|
// ============================================================
|
|
|
|
void wgtDropdownAddItem(WidgetT *w, const char *text) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
arrput(d->ownedItems, strdup(text ? text : ""));
|
|
dropdownSyncOwned(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownClear
|
|
// ============================================================
|
|
|
|
void wgtDropdownClear(WidgetT *w) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
|
|
free(d->ownedItems[i]);
|
|
}
|
|
arrsetlen(d->ownedItems, 0);
|
|
dropdownSyncOwned(w);
|
|
d->selectedIdx = -1;
|
|
d->scrollPos = 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownGetItem
|
|
// ============================================================
|
|
|
|
const char *wgtDropdownGetItem(const WidgetT *w, int32_t idx) {
|
|
if (!w || w->type != sTypeId) { return ""; }
|
|
const DropdownDataT *d = (const DropdownDataT *)w->data;
|
|
if (idx < 0 || idx >= d->itemCount) { return ""; }
|
|
return d->items[idx] ? d->items[idx] : "";
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownGetItemCount
|
|
// ============================================================
|
|
|
|
int32_t wgtDropdownGetItemCount(const WidgetT *w) {
|
|
if (!w || w->type != sTypeId) { return 0; }
|
|
const DropdownDataT *d = (const DropdownDataT *)w->data;
|
|
return d->itemCount;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownRemoveItem
|
|
// ============================================================
|
|
|
|
void wgtDropdownRemoveItem(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sTypeId);
|
|
DropdownDataT *d = (DropdownDataT *)w->data;
|
|
if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
|
|
free(d->ownedItems[idx]);
|
|
arrdel(d->ownedItems, idx);
|
|
dropdownSyncOwned(w);
|
|
if (d->selectedIdx >= d->itemCount) {
|
|
d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 index);
|
|
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 = wgtDropdown,
|
|
.setItems = wgtDropdownSetItems,
|
|
.getSelected = wgtDropdownGetSelected,
|
|
.setSelected = wgtDropdownSetSelected,
|
|
.addItem = wgtDropdownAddItem,
|
|
.removeItem = wgtDropdownRemoveItem,
|
|
.clear = wgtDropdownClear,
|
|
.getItem = wgtDropdownGetItem,
|
|
.getItemCount = wgtDropdownGetItemCount
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "ListIndex", WGT_IFACE_INT, (void *)wgtDropdownGetSelected, (void *)wgtDropdownSetSelected, NULL }
|
|
};
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "AddItem", WGT_SIG_STR, (void *)wgtDropdownAddItem },
|
|
{ "Clear", WGT_SIG_VOID, (void *)wgtDropdownClear },
|
|
{ "List", WGT_SIG_RET_STR_INT, (void *)wgtDropdownGetItem },
|
|
{ "ListCount", WGT_SIG_RET_INT, (void *)wgtDropdownGetItemCount },
|
|
{ "RemoveItem", WGT_SIG_INT, (void *)wgtDropdownRemoveItem },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "DropDown",
|
|
.props = sProps,
|
|
.propCount = 1,
|
|
.methods = sMethods,
|
|
.methodCount = 5,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassDropdown);
|
|
wgtRegisterApi("dropdown", &sApi);
|
|
wgtRegisterIface("dropdown", &sIface);
|
|
}
|