// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // listHelp.c -- Shared list/dropdown helper functions // // Implements dropdown arrow drawing, item length scanning, keyboard // navigation, and popup list painting used by ListBox, Dropdown, // ComboBox, ListView, and TreeView widgets. #include "listHelp.h" #include "../texthelp/textHelp.h" #include "stb_ds_wrap.h" #include #include // ============================================================ // Prototypes // ============================================================ char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count); void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color); void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); int32_t widgetMaxItemLen(const char **items, int32_t count); int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos); int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx); // Rebuild an internally-owned stb_ds string array as a deep copy of an // external source array. Shared by ListBox (ownedItems) and ListView // (ownedCells) so the free-old/reset/strdup adoption sequence lives in one // place. Returns the (possibly reallocated) owned array. char **widgetAdoptOwnedStrings(char **owned, const char **src, int32_t count) { for (int32_t i = 0; i < (int32_t)arrlen(owned); i++) { free(owned[i]); } arrsetlen(owned, 0); for (int32_t i = 0; i < count; i++) { const char *s = src ? src[i] : NULL; arrput(owned, strdup(s ? s : "")); } return owned; } // Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels // wide across 4 rows) centered at the given position. Used by both // Dropdown and ComboBox for the drop button arrow glyph. void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) { for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color); } } // Calculates the screen rectangle for a dropdown/combobox popup list. // Shared between Dropdown and ComboBox since they have identical // popup positioning logic. void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t itemCount, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) { int32_t visibleItems = itemCount; if (visibleItems > DROPDOWN_MAX_VISIBLE) { visibleItems = DROPDOWN_MAX_VISIBLE; } if (visibleItems < 1) { visibleItems = 1; } *popX = w->x; *popW = w->w; *popH = visibleItems * font->charHeight + 4; // Add scrollbar width when list exceeds visible area if (itemCount > DROPDOWN_MAX_VISIBLE) { *popW += POPUP_SCROLLBAR_W; } if (w->y + w->h + *popH <= contentH) { *popY = w->y + w->h; } else { *popY = w->y - *popH; if (*popY < 0) { *popY = 0; } } } // Scans an array of string items and returns the maximum strlen. // Shared by ListBox, Dropdown, and ComboBox to cache the widest // item length for calcMinSize without duplicating the loop. int32_t widgetMaxItemLen(const char **items, int32_t count) { 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; } } return maxLen; } // Shared keyboard navigation for list-like widgets (ListBox, Dropdown, // ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic // so each widget doesn't have to reimplement index clamping. // // Key values use the 0x100 flag to mark extended scan codes (arrow // keys, Home, End, etc.) -- this is the DVX convention for passing // scan codes through the same int32_t channel as ASCII values. // // Returns -1 for unrecognized keys so callers can check whether the // key was consumed. int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) { // An empty list has no valid index; leave the selection unchanged so // callers treat it as 'no navigation' instead of selecting index 0. if (count <= 0) { return current; } if (key == KEY_DOWN) { // Down arrow if (current < count - 1) { return current + 1; } return current < 0 ? 0 : current; } if (key == KEY_UP) { // Up arrow if (current > 0) { return current - 1; } return current < 0 ? 0 : current; } if (key == KEY_HOME) { // Home return 0; } if (key == KEY_END) { // End return count - 1; } if (key == KEY_PGDN) { // Page Down int32_t n = current + pageSize; return n >= count ? count - 1 : n; } if (key == KEY_PGUP) { // Page Up int32_t n = current - pageSize; return n < 0 ? 0 : n; } return -1; } // Shared popup list painting for Dropdown and ComboBox. void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) { bool hasScrollbar = (itemCount > DROPDOWN_MAX_VISIBLE); int32_t visibleItems = popH / font->charHeight; int32_t listW = hasScrollbar ? popW - POPUP_SCROLLBAR_W : popW; // Draw popup border (covers item area only, not scrollbar) BevelStyleT bevel; bevel.highlight = colors->windowHighlight; bevel.shadow = colors->windowShadow; bevel.face = colors->contentBg; bevel.width = 2; drawBevel(d, ops, popX, popY, listW, popH, &bevel); // Draw items int32_t textX = popX + TEXT_INPUT_PAD; int32_t textY = popY + 2; int32_t textW = listW - TEXT_INPUT_PAD * 2 - 4; for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { int32_t idx = scrollPos + i; int32_t iy = textY + i * font->charHeight; uint32_t ifg = colors->contentFg; uint32_t ibg = colors->contentBg; if (idx == hoverIdx) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg); } drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); } // Draw scrollbar if (hasScrollbar) { wmDrawVScrollbarAt(d, ops, colors, popX + listW, popY, popH, scrollPos, visibleItems, itemCount); } } bool widgetPopupScrollbarClick(int32_t x, int32_t y, int32_t popX, int32_t popY, int32_t popW, int32_t popH, int32_t itemCount, int32_t visibleItems, int32_t *scrollPos) { if (itemCount <= DROPDOWN_MAX_VISIBLE) { return false; } int32_t listW = popW - POPUP_SCROLLBAR_W; int32_t sbX = popX + listW; if (x < sbX || x >= sbX + POPUP_SCROLLBAR_W) { return false; } int32_t maxScroll = itemCount - visibleItems; int32_t relY = y - popY; if (relY < POPUP_SCROLLBAR_W) { // Up arrow if (*scrollPos > 0) { (*scrollPos)--; } } else if (relY >= popH - POPUP_SCROLLBAR_W) { // Down arrow if (*scrollPos < maxScroll) { (*scrollPos)++; } } else { // Trough — page up/down based on which side of thumb int32_t trackLen = popH - POPUP_SCROLLBAR_W * 2; int32_t thumbSize = (int32_t)(((int64_t)visibleItems * trackLen) / itemCount); if (thumbSize < POPUP_SCROLLBAR_W) { thumbSize = POPUP_SCROLLBAR_W; } int32_t thumbPos = 0; if (maxScroll > 0) { thumbPos = (int32_t)(((int64_t)(*scrollPos) * (trackLen - thumbSize)) / maxScroll); } int32_t clickInTrack = relY - POPUP_SCROLLBAR_W; if (clickInTrack < thumbPos) { // Page up *scrollPos -= visibleItems; if (*scrollPos < 0) { *scrollPos = 0; } } else if (clickInTrack >= thumbPos + thumbSize) { // Page down *scrollPos += visibleItems; if (*scrollPos > maxScroll) { *scrollPos = maxScroll; } } } return true; } int32_t widgetTypeAheadSearch(char ch, const char **items, int32_t itemCount, int32_t currentIdx) { if (itemCount <= 0 || !items) { return -1; } char upper = (ch >= 'a' && ch <= 'z') ? ch - 32 : ch; // Search forward from currentIdx+1, wrapping around for (int32_t i = 1; i <= itemCount; i++) { int32_t idx = (currentIdx + i) % itemCount; char first = items[idx][0]; if (first >= 'a' && first <= 'z') { first -= 32; } if (first == upper) { return idx; } } return -1; }