DVX_GUI/dvx/widgets/widgetComboBox.c

421 lines
14 KiB
C

// widgetComboBox.c — ComboBox widget (editable text + dropdown list)
#include "widgetInternal.h"
// ============================================================
// wgtComboBox
// ============================================================
WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
WidgetT *w = widgetAlloc(parent, WidgetComboBoxE);
if (w) {
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
w->as.comboBox.buf = (char *)malloc(bufSize);
w->as.comboBox.undoBuf = (char *)malloc(bufSize);
w->as.comboBox.bufSize = bufSize;
if (!w->as.comboBox.buf || !w->as.comboBox.undoBuf) {
free(w->as.comboBox.buf);
free(w->as.comboBox.undoBuf);
w->as.comboBox.buf = NULL;
w->as.comboBox.undoBuf = NULL;
} else {
w->as.comboBox.buf[0] = '\0';
}
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
w->as.comboBox.selectedIdx = -1;
w->weight = 100;
}
return w;
}
// ============================================================
// wgtComboBoxGetSelected
// ============================================================
int32_t wgtComboBoxGetSelected(const WidgetT *w) {
VALIDATE_WIDGET(w, WidgetComboBoxE, -1);
return w->as.comboBox.selectedIdx;
}
// ============================================================
// wgtComboBoxSetItems
// ============================================================
void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) {
VALIDATE_WIDGET_VOID(w, WidgetComboBoxE);
w->as.comboBox.items = items;
w->as.comboBox.itemCount = count;
// Cache max item strlen to avoid recomputing in calcMinSize
int32_t maxLen = 0;
for (int32_t i = 0; i < count; i++) {
int32_t slen = (int32_t)strlen(items[i]);
if (slen > maxLen) {
maxLen = slen;
}
}
w->as.comboBox.maxItemLen = maxLen;
if (w->as.comboBox.selectedIdx >= count) {
w->as.comboBox.selectedIdx = -1;
}
wgtInvalidate(w);
}
// ============================================================
// wgtComboBoxSetSelected
// ============================================================
void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) {
VALIDATE_WIDGET_VOID(w, WidgetComboBoxE);
w->as.comboBox.selectedIdx = idx;
// Copy selected item text to buffer
if (idx >= 0 && idx < w->as.comboBox.itemCount && w->as.comboBox.buf) {
strncpy(w->as.comboBox.buf, w->as.comboBox.items[idx], w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
}
wgtInvalidatePaint(w);
}
// ============================================================
// widgetComboBoxCalcMinSize
// ============================================================
void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t maxItemW = w->as.comboBox.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) {
free(w->as.comboBox.buf);
free(w->as.comboBox.undoBuf);
}
// ============================================================
// widgetComboBoxGetText
// ============================================================
const char *widgetComboBoxGetText(const WidgetT *w) {
return w->as.comboBox.buf ? w->as.comboBox.buf : "";
}
// ============================================================
// widgetComboBoxOnKey
// ============================================================
void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->as.comboBox.open) {
if (key == (0x48 | 0x100)) {
if (w->as.comboBox.hoverIdx > 0) {
w->as.comboBox.hoverIdx--;
if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
}
}
wgtInvalidatePaint(w);
return;
}
if (key == (0x50 | 0x100)) {
if (w->as.comboBox.hoverIdx < w->as.comboBox.itemCount - 1) {
w->as.comboBox.hoverIdx++;
if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
}
wgtInvalidatePaint(w);
return;
}
if (key == 0x0D) {
int32_t idx = w->as.comboBox.hoverIdx;
if (idx >= 0 && idx < w->as.comboBox.itemCount) {
w->as.comboBox.selectedIdx = idx;
const char *itemText = w->as.comboBox.items[idx];
strncpy(w->as.comboBox.buf, itemText, w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
}
w->as.comboBox.open = false;
sOpenPopup = NULL;
if (w->onChange) {
w->onChange(w);
}
wgtInvalidatePaint(w);
return;
}
}
// Down arrow on closed combobox opens the popup
if (!w->as.comboBox.open && key == (0x50 | 0x100)) {
w->as.comboBox.open = true;
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
sOpenPopup = w;
if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
}
if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) {
w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx;
}
wgtInvalidatePaint(w);
return;
}
// Text editing (when popup is closed, or non-navigation keys with popup open)
if (!w->as.comboBox.buf) {
return;
}
clearOtherSelections(w);
widgetTextEditOnKey(w, key, mod, w->as.comboBox.buf, w->as.comboBox.bufSize,
&w->as.comboBox.len, &w->as.comboBox.cursorPos,
&w->as.comboBox.scrollOff,
&w->as.comboBox.selStart, &w->as.comboBox.selEnd,
w->as.comboBox.undoBuf, &w->as.comboBox.undoLen,
&w->as.comboBox.undoCursor);
}
// ============================================================
// widgetComboBoxOnMouse
// ============================================================
void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
w->focused = true;
// 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
w->as.comboBox.open = !w->as.comboBox.open;
w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx;
sOpenPopup = w->as.comboBox.open ? w : NULL;
} else {
// Text area click — focus for editing
clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font;
int32_t relX = vx - w->x - TEXT_INPUT_PAD;
int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff;
if (charPos < 0) {
charPos = 0;
}
if (charPos > w->as.comboBox.len) {
charPos = w->as.comboBox.len;
}
int32_t clicks = multiClickDetect(vx, vy);
if (clicks >= 3) {
// Triple-click: select all (single line)
w->as.comboBox.selStart = 0;
w->as.comboBox.selEnd = w->as.comboBox.len;
w->as.comboBox.cursorPos = w->as.comboBox.len;
sDragTextSelect = NULL;
return;
}
if (clicks == 2 && w->as.comboBox.buf) {
// Double-click: select word
int32_t ws = wordStart(w->as.comboBox.buf, charPos);
int32_t we = wordEnd(w->as.comboBox.buf, w->as.comboBox.len, charPos);
w->as.comboBox.selStart = ws;
w->as.comboBox.selEnd = we;
w->as.comboBox.cursorPos = we;
sDragTextSelect = NULL;
return;
}
// Single click: place cursor + start drag-select
w->as.comboBox.cursorPos = charPos;
w->as.comboBox.selStart = charPos;
w->as.comboBox.selEnd = charPos;
sDragTextSelect = w;
}
}
// ============================================================
// widgetComboBoxPaint
// ============================================================
void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
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(d, ops, w->x, w->y, textAreaW, w->h, &bevel);
// Draw text content
if (w->as.comboBox.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 = w->as.comboBox.scrollOff;
int32_t len = w->as.comboBox.len - off;
if (len > maxChars) {
len = maxChars;
}
// Selection range
int32_t selLo = -1;
int32_t selHi = -1;
if (w->as.comboBox.selStart >= 0 && w->as.comboBox.selEnd >= 0 && w->as.comboBox.selStart != w->as.comboBox.selEnd) {
selLo = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selStart : w->as.comboBox.selEnd;
selHi = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selEnd : w->as.comboBox.selStart;
}
// Draw up to 3 runs: before selection, selection, after selection
int32_t visSelLo = selLo - off;
int32_t visSelHi = selHi - off;
if (visSelLo < 0) { visSelLo = 0; }
if (visSelHi > len) { visSelHi = len; }
if (selLo >= 0 && visSelLo < visSelHi) {
if (visSelLo > 0) {
drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, visSelLo, fg, bg, true);
}
drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, w->as.comboBox.buf + off + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
if (visSelHi < len) {
drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, w->as.comboBox.buf + off + visSelHi, len - visSelHi, fg, bg, true);
}
} else {
drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, len, fg, bg, true);
}
// Draw cursor
if (w->focused && w->enabled && !w->as.comboBox.open) {
int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth;
if (cursorX >= w->x + TEXT_INPUT_PAD &&
cursorX < w->x + textAreaW - TEXT_INPUT_PAD) {
drawVLine(d, ops, cursorX, textY, font->charHeight, fg);
}
}
}
// Drop button
BevelStyleT btnBevel;
btnBevel.highlight = w->as.comboBox.open ? colors->windowShadow : colors->windowHighlight;
btnBevel.shadow = w->as.comboBox.open ? colors->windowHighlight : colors->windowShadow;
btnBevel.face = colors->buttonFace;
btnBevel.width = 2;
drawBevel(d, 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;
for (int32_t i = 0; i < 4; i++) {
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
}
}
// ============================================================
// widgetComboBoxPaintPopup
// ============================================================
void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
int32_t popX;
int32_t popY;
int32_t popW;
int32_t popH;
widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH);
widgetPaintPopupList(d, ops, font, colors, popX, popY, popW, popH, w->as.comboBox.items, w->as.comboBox.itemCount, w->as.comboBox.hoverIdx, w->as.comboBox.listScrollPos);
}
// ============================================================
// widgetComboBoxSetText
// ============================================================
void widgetComboBoxSetText(WidgetT *w, const char *text) {
if (w->as.comboBox.buf) {
strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1);
w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0';
w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0;
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
}
}