// 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; } }