290 lines
8.4 KiB
C
290 lines
8.4 KiB
C
// 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 <string.h>
|
|
|
|
|
|
// ============================================================
|
|
// widgetDrawDropdownArrow
|
|
// ============================================================
|
|
//
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetMaxItemLen
|
|
// ============================================================
|
|
//
|
|
// 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetNavigateIndex
|
|
// ============================================================
|
|
//
|
|
// 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) {
|
|
if (key == (0x50 | 0x100)) {
|
|
// Down arrow
|
|
if (current < count - 1) {
|
|
return current + 1;
|
|
}
|
|
|
|
return current < 0 ? 0 : current;
|
|
}
|
|
|
|
if (key == (0x48 | 0x100)) {
|
|
// Up arrow
|
|
if (current > 0) {
|
|
return current - 1;
|
|
}
|
|
|
|
return current < 0 ? 0 : current;
|
|
}
|
|
|
|
if (key == (0x47 | 0x100)) {
|
|
// Home
|
|
return 0;
|
|
}
|
|
|
|
if (key == (0x4F | 0x100)) {
|
|
// End
|
|
return count - 1;
|
|
}
|
|
|
|
if (key == (0x51 | 0x100)) {
|
|
// Page Down
|
|
int32_t n = current + pageSize;
|
|
return n >= count ? count - 1 : n;
|
|
}
|
|
|
|
if (key == (0x49 | 0x100)) {
|
|
// Page Up
|
|
int32_t n = current - pageSize;
|
|
return n < 0 ? 0 : n;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTypeAheadSearch
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetPaintPopupList
|
|
// ============================================================
|
|
//
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetPopupScrollbarClick
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownPopupRect
|
|
// ============================================================
|
|
//
|
|
// 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;
|
|
}
|
|
}
|
|
}
|