252 lines
8 KiB
C
252 lines
8 KiB
C
// widgetDropdown.c — Dropdown (select) widget
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdown
|
|
// ============================================================
|
|
|
|
WidgetT *wgtDropdown(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, WidgetDropdownE);
|
|
|
|
if (w) {
|
|
w->as.dropdown.selectedIdx = -1;
|
|
w->as.dropdown.hoverIdx = -1;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownGetSelected
|
|
// ============================================================
|
|
|
|
int32_t wgtDropdownGetSelected(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, WidgetDropdownE, -1);
|
|
|
|
return w->as.dropdown.selectedIdx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownSetItems
|
|
// ============================================================
|
|
|
|
void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count) {
|
|
VALIDATE_WIDGET_VOID(w, WidgetDropdownE);
|
|
|
|
w->as.dropdown.items = items;
|
|
w->as.dropdown.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.dropdown.maxItemLen = maxLen;
|
|
|
|
if (w->as.dropdown.selectedIdx >= count) {
|
|
w->as.dropdown.selectedIdx = -1;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtDropdownSetSelected
|
|
// ============================================================
|
|
|
|
void wgtDropdownSetSelected(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, WidgetDropdownE);
|
|
|
|
w->as.dropdown.selectedIdx = idx;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownGetText
|
|
// ============================================================
|
|
|
|
const char *widgetDropdownGetText(const WidgetT *w) {
|
|
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
|
|
return w->as.dropdown.items[w->as.dropdown.selectedIdx];
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownCalcMinSize
|
|
// ============================================================
|
|
|
|
void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
// Width: widest item + button width + border
|
|
int32_t maxItemW = w->as.dropdown.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
|
|
// ============================================================
|
|
|
|
void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
|
|
if (w->as.dropdown.open) {
|
|
// Popup is open — navigate items
|
|
if (key == (0x48 | 0x100)) {
|
|
if (w->as.dropdown.hoverIdx > 0) {
|
|
w->as.dropdown.hoverIdx--;
|
|
|
|
if (w->as.dropdown.hoverIdx < w->as.dropdown.scrollPos) {
|
|
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx;
|
|
}
|
|
}
|
|
} else if (key == (0x50 | 0x100)) {
|
|
if (w->as.dropdown.hoverIdx < w->as.dropdown.itemCount - 1) {
|
|
w->as.dropdown.hoverIdx++;
|
|
|
|
if (w->as.dropdown.hoverIdx >= w->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
|
|
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
|
}
|
|
}
|
|
} else if (key == 0x0D || key == ' ') {
|
|
w->as.dropdown.selectedIdx = w->as.dropdown.hoverIdx;
|
|
w->as.dropdown.open = false;
|
|
sOpenPopup = NULL;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else {
|
|
// Popup is closed
|
|
if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) {
|
|
w->as.dropdown.open = true;
|
|
w->as.dropdown.hoverIdx = w->as.dropdown.selectedIdx;
|
|
sOpenPopup = w;
|
|
|
|
if (w->as.dropdown.hoverIdx >= w->as.dropdown.scrollPos + DROPDOWN_MAX_VISIBLE) {
|
|
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx - DROPDOWN_MAX_VISIBLE + 1;
|
|
}
|
|
|
|
if (w->as.dropdown.hoverIdx < w->as.dropdown.scrollPos) {
|
|
w->as.dropdown.scrollPos = w->as.dropdown.hoverIdx;
|
|
}
|
|
} else if (key == (0x48 | 0x100)) {
|
|
if (w->as.dropdown.selectedIdx > 0) {
|
|
w->as.dropdown.selectedIdx--;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownOnMouse
|
|
// ============================================================
|
|
|
|
void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
|
|
(void)root;
|
|
(void)vx;
|
|
(void)vy;
|
|
w->focused = true;
|
|
|
|
// If this dropdown's popup was just closed by click-outside, don't re-open
|
|
if (w == sClosedPopup) {
|
|
return;
|
|
}
|
|
|
|
w->as.dropdown.open = !w->as.dropdown.open;
|
|
w->as.dropdown.hoverIdx = w->as.dropdown.selectedIdx;
|
|
sOpenPopup = w->as.dropdown.open ? w : NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownPaint
|
|
// ============================================================
|
|
|
|
void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
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(d, ops, w->x, w->y, textAreaW, w->h, &bevel);
|
|
|
|
// Draw selected item text
|
|
if (w->as.dropdown.selectedIdx >= 0 && w->as.dropdown.selectedIdx < w->as.dropdown.itemCount) {
|
|
int32_t textX = w->x + TEXT_INPUT_PAD;
|
|
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
|
|
|
if (!w->enabled) {
|
|
drawTextEmbossed(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], colors);
|
|
} else {
|
|
drawText(d, ops, font, textX, textY, w->as.dropdown.items[w->as.dropdown.selectedIdx], fg, bg, true);
|
|
}
|
|
}
|
|
|
|
// Drop button
|
|
BevelStyleT btnBevel;
|
|
btnBevel.highlight = w->as.dropdown.open ? colors->windowShadow : colors->windowHighlight;
|
|
btnBevel.shadow = w->as.dropdown.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 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;
|
|
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawHLine(d, ops, arrowX - 3 + i, arrowY + i, 7 - i * 2, arrowFg);
|
|
}
|
|
|
|
if (w->focused) {
|
|
drawFocusRect(d, ops, w->x + 3, w->y + 3, textAreaW - 6, w->h - 6, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownPaintPopup
|
|
// ============================================================
|
|
|
|
void widgetDropdownPaintPopup(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.dropdown.items, w->as.dropdown.itemCount, w->as.dropdown.hoverIdx, w->as.dropdown.scrollPos);
|
|
}
|