DVX_GUI/widgets/dropdown/widgetDropdown.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);
}