#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 "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, w->w - DROPDOWN_BTN_WIDTH); } // ============================================================ // widgetComboBoxOnMouse // ============================================================ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { w->focused = true; 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->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, 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); } // ============================================================ // 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); }