DVX_GUI/widgets/comboBox/widgetComboBox.c

651 lines
21 KiB
C

#define DVX_WIDGET_IMPL
// widgetComboBox.c -- ComboBox widget (editable text + dropdown list)
//
// Combines a single-line text input with a dropdown list. The text area
// supports full editing (cursor movement, selection, undo, clipboard) via
// the shared widgetTextEditOnKey helper, while the dropdown button opens
// a popup list overlay.
//
// This is a "combo" box in the Windows sense: the user can either type a
// value or select from the list. When an item is selected from the list,
// its text is copied into the edit buffer. The edit buffer is independently
// allocated (malloc'd) so the user can modify the text after selecting.
//
// The popup list is painted as an overlay (widgetComboBoxPaintPopup) that
// renders on top of all other widgets. Popup visibility is coordinated
// through the sOpenPopup global -- only one popup can be open at a time.
// The sClosedPopup mechanism prevents click-to-close from immediately
// reopening the popup when the close click lands on the dropdown button.
//
// Text selection supports single-click (cursor placement + drag start),
// double-click (word select), and triple-click (select all). Drag-select
// is tracked via the sDragWidget global.
#include "dvxWgtP.h"
#include "../texthelp/textHelp.h"
#include "../listhelp/listHelp.h"
#include "stb_ds_wrap.h"
static int32_t sTypeId = -1;
typedef struct {
char *buf;
int32_t bufSize;
int32_t len;
int32_t cursorPos;
int32_t scrollOff;
int32_t selStart;
int32_t selEnd;
char *undoBuf;
int32_t undoLen;
int32_t undoCursor;
const char **items;
int32_t itemCount;
int32_t selectedIdx;
bool open;
int32_t hoverIdx;
int32_t listScrollPos;
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
} ComboBoxDataT;
// ============================================================
// widgetComboBoxCalcMinSize
// ============================================================
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
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;
}
// ============================================================
// widgetComboBoxDestroy
// ============================================================
void widgetComboBoxDestroy(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)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->buf);
free(d->undoBuf);
free(d);
w->data = NULL;
}
}
// ============================================================
// widgetComboBoxGetText
// ============================================================
const char *widgetComboBoxGetText(const WidgetT *w) {
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
return d->buf ? d->buf : "";
}
// ============================================================
// widgetComboBoxOnKey
// ============================================================
// Key handling has two modes: when the popup is open, Up/Down navigate the list
// and Enter confirms the selection. When closed, keys go to the text editor
// (via widgetTextEditOnKey) except Down-arrow which opens the popup. This split
// behavior is necessary because the same widget must serve as both a text input
// and a list selector depending on popup state.
// Key codes: 0x48|0x100 = Up, 0x50|0x100 = Down (BIOS scan codes with extended bit).
void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (d->open) {
if (key == (0x48 | 0x100)) {
if (d->hoverIdx > 0) {
d->hoverIdx--;
if (d->hoverIdx < d->listScrollPos) {
d->listScrollPos = d->hoverIdx;
}
}
wgtInvalidatePaint(w);
return;
}
if (key == (0x50 | 0x100)) {
if (d->hoverIdx < d->itemCount - 1) {
d->hoverIdx++;
if (d->hoverIdx >= d->listScrollPos + DROPDOWN_MAX_VISIBLE) {
d->listScrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
wgtInvalidatePaint(w);
return;
}
if (key == 0x0D) {
int32_t idx = d->hoverIdx;
if (idx >= 0 && idx < d->itemCount) {
d->selectedIdx = idx;
const char *itemText = d->items[idx];
strncpy(d->buf, itemText, d->bufSize - 1);
d->buf[d->bufSize - 1] = '\0';
d->len = (int32_t)strlen(d->buf);
d->cursorPos = d->len;
d->scrollOff = 0;
d->selStart = -1;
d->selEnd = -1;
}
d->open = false;
sOpenPopup = NULL;
if (w->onChange) {
w->onChange(w);
}
wgtInvalidatePaint(w);
return;
}
}
// Down arrow on closed combobox opens the popup
if (!d->open && key == (0x50 | 0x100)) {
d->open = true;
d->hoverIdx = d->selectedIdx;
sOpenPopup = w;
if (d->hoverIdx >= d->listScrollPos + DROPDOWN_MAX_VISIBLE) {
d->listScrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (d->hoverIdx < d->listScrollPos) {
d->listScrollPos = d->hoverIdx;
}
wgtInvalidatePaint(w);
return;
}
// Text editing (when popup is closed, or non-navigation keys with popup open)
if (!d->buf) {
return;
}
clearOtherSelections(w);
widgetTextEditOnKey(w, key, mod, d->buf, d->bufSize,
&d->len, &d->cursorPos,
&d->scrollOff,
&d->selStart, &d->selEnd,
d->undoBuf, &d->undoLen,
&d->undoCursor,
w->w - DROPDOWN_BTN_WIDTH);
}
// ============================================================
// widgetComboBoxOnMouse
// ============================================================
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
sFocusedWidget = w;
ComboBoxDataT *d = (ComboBoxDataT *)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->listScrollPos + (vy - popY - 2) / font->charHeight;
if (itemIdx >= 0 && itemIdx < d->itemCount) {
d->selectedIdx = itemIdx;
int32_t slen = (int32_t)strlen(d->items[itemIdx]);
if (slen >= d->bufSize) {
slen = d->bufSize - 1;
}
memcpy(d->buf, d->items[itemIdx], slen);
d->buf[slen] = '\0';
d->len = slen;
d->cursorPos = slen;
d->scrollOff = 0;
d->selStart = -1;
d->selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
}
d->open = false;
sOpenPopup = NULL;
return;
}
// Check if click is on the button area
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
if (vx >= w->x + textAreaW) {
if (w == sClosedPopup) {
return;
}
d->open = true;
d->hoverIdx = d->selectedIdx;
sOpenPopup = w;
} else {
// Text area click -- focus for editing
clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData;
widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, d->buf, d->len, d->scrollOff, &d->cursorPos, &d->selStart, &d->selEnd, true, true);
}
}
// ============================================================
// widgetComboBoxPaint
// ============================================================
// Paint: two regions side-by-side -- a sunken text area (left) and a raised
// dropdown button (right). The text area renders the edit buffer with optional
// selection highlighting (up to 3 text runs: pre-selection, selection,
// post-selection). The dropdown button has a small triangular arrow glyph
// drawn as horizontal lines of decreasing width. When the popup is open,
// the button bevel is inverted (sunken) to show it's active.
void widgetComboBoxPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow;
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 text content
if (d->buf) {
int32_t textX = w->x + TEXT_INPUT_PAD;
int32_t textY = w->y + (w->h - font->charHeight) / 2;
int32_t maxChars = (textAreaW - TEXT_INPUT_PAD * 2 - 4) / font->charWidth;
int32_t off = d->scrollOff;
int32_t len = d->len - off;
if (len > maxChars) {
len = maxChars;
}
widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, d->buf + off, len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w == sFocusedWidget && w->enabled && !d->open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD);
}
// 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
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);
}
// ============================================================
// widgetComboBoxPaintPopup
// ============================================================
void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
ComboBoxDataT *d = (ComboBoxDataT *)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->listScrollPos);
}
// ============================================================
// widgetComboBoxSetText
// ============================================================
void widgetComboBoxSetText(WidgetT *w, const char *text) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (d->buf) {
strncpy(d->buf, text, d->bufSize - 1);
d->buf[d->bufSize - 1] = '\0';
d->len = (int32_t)strlen(d->buf);
d->cursorPos = d->len;
d->scrollOff = 0;
d->selStart = -1;
d->selEnd = -1;
}
}
// ============================================================
// widgetComboBoxAccelActivate
// ============================================================
void widgetComboBoxAccelActivate(WidgetT *w, WidgetT *root) {
(void)root;
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
d->open = true;
d->hoverIdx = d->selectedIdx;
sOpenPopup = w;
wgtInvalidatePaint(w);
}
// ============================================================
// widgetComboBoxClosePopup
// ============================================================
void widgetComboBoxClosePopup(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
d->open = false;
wgtInvalidatePaint(w);
}
// ============================================================
// DXE registration
// ============================================================
static void widgetComboBoxGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
widgetDropdownPopupRect((WidgetT *)w, font, contentH, d->itemCount, popX, popY, popW, popH);
}
static bool widgetComboBoxClearSelection(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (d->selStart >= 0 && d->selEnd >= 0 && d->selStart != d->selEnd) {
d->selStart = -1;
d->selEnd = -1;
return true;
}
return false;
}
static void widgetComboBoxOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
(void)vy;
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
AppContextT *ctx = wgtGetContext(w);
int32_t fieldW = w->w - DROPDOWN_BTN_WIDTH;
int32_t maxChars = (fieldW - TEXT_INPUT_PAD * 2) / ctx->font.charWidth;
widgetTextEditDragUpdateLine(vx, w->x + TEXT_INPUT_PAD, maxChars, &ctx->font, d->len, &d->cursorPos, &d->scrollOff, &d->selEnd);
}
static const WidgetClassT sClassComboBox = {
.version = WGT_CLASS_VERSION,
.flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE,
.handlers = {
[WGT_METHOD_PAINT] = (void *)widgetComboBoxPaint,
[WGT_METHOD_PAINT_OVERLAY] = (void *)widgetComboBoxPaintPopup,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetComboBoxCalcMinSize,
[WGT_METHOD_ON_MOUSE] = (void *)widgetComboBoxOnMouse,
[WGT_METHOD_ON_KEY] = (void *)widgetComboBoxOnKey,
[WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetComboBoxAccelActivate,
[WGT_METHOD_DESTROY] = (void *)widgetComboBoxDestroy,
[WGT_METHOD_GET_TEXT] = (void *)widgetComboBoxGetText,
[WGT_METHOD_SET_TEXT] = (void *)widgetComboBoxSetText,
[WGT_METHOD_CLOSE_POPUP] = (void *)widgetComboBoxClosePopup,
[WGT_METHOD_CLEAR_SELECTION] = (void *)widgetComboBoxClearSelection,
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetComboBoxOnDragUpdate,
[WGT_METHOD_GET_POPUP_RECT] = (void *)widgetComboBoxGetPopupRect,
}
};
// ============================================================
// Widget creation functions
// ============================================================
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
WidgetT *w = widgetAlloc(parent, sTypeId);
if (w) {
ComboBoxDataT *d = (ComboBoxDataT *)calloc(1, sizeof(ComboBoxDataT));
if (!d) {
return w;
}
w->data = d;
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
d->buf = (char *)malloc(bufSize);
d->undoBuf = (char *)malloc(bufSize);
d->bufSize = bufSize;
if (!d->buf || !d->undoBuf) {
free(d->buf);
free(d->undoBuf);
d->buf = NULL;
d->undoBuf = NULL;
} else {
d->buf[0] = '\0';
}
d->selStart = -1;
d->selEnd = -1;
d->selectedIdx = -1;
w->weight = 100;
}
return w;
}
int32_t wgtComboBoxGetSelected(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, -1);
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
return d->selectedIdx;
}
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
d->items = items;
d->itemCount = count;
d->maxItemLen = widgetMaxItemLen(items, count);
if (d->selectedIdx >= count) {
d->selectedIdx = -1;
}
wgtInvalidate(w);
}
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
d->selectedIdx = idx;
// Copy selected item text to buffer
if (idx >= 0 && idx < d->itemCount && d->buf) {
strncpy(d->buf, d->items[idx], d->bufSize - 1);
d->buf[d->bufSize - 1] = '\0';
d->len = (int32_t)strlen(d->buf);
d->cursorPos = d->len;
d->scrollOff = 0;
d->selStart = -1;
d->selEnd = -1;
}
wgtInvalidatePaint(w);
}
// ============================================================
// Owned item management
// ============================================================
static void comboBoxSyncOwned(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)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);
}
void wgtComboBoxAddItem(WidgetT *w, const char *text) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
arrput(d->ownedItems, strdup(text ? text : ""));
comboBoxSyncOwned(w);
}
void wgtComboBoxClear(WidgetT *w) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
for (int32_t i = 0; i < (int32_t)arrlen(d->ownedItems); i++) {
free(d->ownedItems[i]);
}
arrsetlen(d->ownedItems, 0);
comboBoxSyncOwned(w);
d->selectedIdx = -1;
d->listScrollPos = 0;
}
const char *wgtComboBoxGetItem(const WidgetT *w, int32_t idx) {
if (!w || w->type != sTypeId) { return ""; }
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
if (idx < 0 || idx >= d->itemCount) { return ""; }
return d->items[idx] ? d->items[idx] : "";
}
int32_t wgtComboBoxGetItemCount(const WidgetT *w) {
if (!w || w->type != sTypeId) { return 0; }
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
return d->itemCount;
}
void wgtComboBoxRemoveItem(WidgetT *w, int32_t idx) {
VALIDATE_WIDGET_VOID(w, sTypeId);
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
if (idx < 0 || idx >= (int32_t)arrlen(d->ownedItems)) { return; }
free(d->ownedItems[idx]);
arrdel(d->ownedItems, idx);
comboBoxSyncOwned(w);
if (d->selectedIdx >= d->itemCount) {
d->selectedIdx = d->itemCount > 0 ? d->itemCount - 1 : -1;
}
}
// ============================================================
// DXE registration
// ============================================================
static const struct {
WidgetT *(*create)(WidgetT *parent, int32_t maxLen);
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 = wgtComboBox,
.setItems = wgtComboBoxSetItems,
.getSelected = wgtComboBoxGetSelected,
.setSelected = wgtComboBoxSetSelected,
.addItem = wgtComboBoxAddItem,
.removeItem = wgtComboBoxRemoveItem,
.clear = wgtComboBoxClear,
.getItem = wgtComboBoxGetItem,
.getItemCount = wgtComboBoxGetItemCount
};
static const WgtPropDescT sProps[] = {
{ "ListIndex", WGT_IFACE_INT, (void *)wgtComboBoxGetSelected, (void *)wgtComboBoxSetSelected, NULL }
};
static const WgtMethodDescT sMethods[] = {
{ "AddItem", WGT_SIG_STR, (void *)wgtComboBoxAddItem },
{ "Clear", WGT_SIG_VOID, (void *)wgtComboBoxClear },
{ "List", WGT_SIG_RET_STR_INT, (void *)wgtComboBoxGetItem },
{ "ListCount", WGT_SIG_RET_INT, (void *)wgtComboBoxGetItemCount },
{ "RemoveItem", WGT_SIG_INT, (void *)wgtComboBoxRemoveItem },
};
static const WgtIfaceT sIface = {
.basName = "ComboBox",
.props = sProps,
.propCount = 1,
.methods = sMethods,
.methodCount = 5,
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT,
.createArgs = { 256 },
.defaultEvent = "Click"
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClassComboBox);
wgtRegisterApi("combobox", &sApi);
wgtRegisterIface("combobox", &sIface);
}