DVX_GUI/widgets/widgetComboBox.c

499 lines
16 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 sDragTextSelect global.
#include "dvxWidgetPlugin.h"
#include "../texthelp/textHelp.h"
#include "../listhelp/listHelp.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;
} 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) {
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);
}
// ============================================================
// widgetComboBoxOnMouse
// ============================================================
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
w->focused = true;
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
// Check if click is on the button area
int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH;
if (vx >= w->x + textAreaW) {
// If this combobox's popup was just closed by click-outside, don't re-open
if (w == sClosedPopup) {
return;
}
// Button click -- toggle popup
d->open = !d->open;
d->hoverIdx = d->selectedIdx;
sOpenPopup = d->open ? w : NULL;
} 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->focused && 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, &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);
}
// ============================================================
// widgetComboBoxGetPopupItemCount
// ============================================================
int32_t widgetComboBoxGetPopupItemCount(const WidgetT *w) {
const ComboBoxDataT *d = (const ComboBoxDataT *)w->data;
return d->itemCount;
}
// ============================================================
// widgetComboBoxOpenPopup
// ============================================================
void widgetComboBoxOpenPopup(WidgetT *w) {
ComboBoxDataT *d = (ComboBoxDataT *)w->data;
d->open = true;
d->hoverIdx = d->selectedIdx;
sOpenPopup = w;
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) {
widgetDropdownPopupRect((WidgetT *)w, font, contentH, popX, popY, popW, popH);
}
static const WidgetClassT sClassComboBox = {
.flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE,
.paint = widgetComboBoxPaint,
.paintOverlay = widgetComboBoxPaintPopup,
.calcMinSize = widgetComboBoxCalcMinSize,
.layout = NULL,
.onMouse = widgetComboBoxOnMouse,
.onKey = widgetComboBoxOnKey,
.onAccelActivate = widgetComboBoxAccelActivate,
.destroy = widgetComboBoxDestroy,
.getText = widgetComboBoxGetText,
.setText = widgetComboBoxSetText,
.openPopup = widgetComboBoxOpenPopup,
.closePopup = widgetComboBoxClosePopup,
.getPopupItemCount = widgetComboBoxGetPopupItemCount,
.getPopupRect = 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);
}
// ============================================================
// 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);
} sApi = {
.create = wgtComboBox,
.setItems = wgtComboBoxSetItems,
.getSelected = wgtComboBoxGetSelected,
.setSelected = wgtComboBoxSetSelected
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClassComboBox);
wgtRegisterApi("combobox", &sApi);
}