// widgetListBox.c — ListBox widget #include "widgetInternal.h" #define LISTBOX_BORDER 2 #define LISTBOX_PAD 2 #define LISTBOX_MIN_ROWS 4 #define LISTBOX_SB_W 14 // ============================================================ // Prototypes // ============================================================ static void listBoxScrollbarThumb(int32_t trackLen, int32_t totalItems, int32_t visibleItems, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize); // ============================================================ // listBoxScrollbarThumb // ============================================================ static void listBoxScrollbarThumb(int32_t trackLen, int32_t totalItems, int32_t visibleItems, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) { *thumbSize = (trackLen * visibleItems) / totalItems; if (*thumbSize < LISTBOX_SB_W) { *thumbSize = LISTBOX_SB_W; } if (*thumbSize > trackLen) { *thumbSize = trackLen; } int32_t maxScroll = totalItems - visibleItems; if (maxScroll > 0) { *thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll; } else { *thumbPos = 0; } } // ============================================================ // wgtListBox // ============================================================ WidgetT *wgtListBox(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, WidgetListBoxE); if (w) { w->as.listBox.selectedIdx = -1; } return w; } // ============================================================ // wgtListBoxGetSelected // ============================================================ int32_t wgtListBoxGetSelected(const WidgetT *w) { if (!w || w->type != WidgetListBoxE) { return -1; } return w->as.listBox.selectedIdx; } // ============================================================ // wgtListBoxSetItems // ============================================================ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { if (!w || w->type != WidgetListBoxE) { return; } w->as.listBox.items = items; w->as.listBox.itemCount = count; if (w->as.listBox.selectedIdx >= count) { w->as.listBox.selectedIdx = count > 0 ? 0 : -1; } if (w->as.listBox.selectedIdx < 0 && count > 0) { w->as.listBox.selectedIdx = 0; } } // ============================================================ // wgtListBoxSetSelected // ============================================================ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) { if (!w || w->type != WidgetListBoxE) { return; } w->as.listBox.selectedIdx = idx; } // ============================================================ // widgetListBoxCalcMinSize // ============================================================ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t maxItemW = font->charWidth * 8; for (int32_t i = 0; i < w->as.listBox.itemCount; i++) { int32_t iw = (int32_t)strlen(w->as.listBox.items[i]) * font->charWidth; if (iw > maxItemW) { maxItemW = iw; } } w->calcMinW = maxItemW + LISTBOX_PAD * 2 + LISTBOX_BORDER * 2 + LISTBOX_SB_W; w->calcMinH = LISTBOX_MIN_ROWS * font->charHeight + LISTBOX_BORDER * 2; } // ============================================================ // widgetListBoxOnKey // ============================================================ void widgetListBoxOnKey(WidgetT *w, int32_t key) { if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) { return; } int32_t sel = w->as.listBox.selectedIdx; if (key == (0x50 | 0x100)) { // Down arrow if (sel < w->as.listBox.itemCount - 1) { w->as.listBox.selectedIdx = sel + 1; } else if (sel < 0) { w->as.listBox.selectedIdx = 0; } } else if (key == (0x48 | 0x100)) { // Up arrow if (sel > 0) { w->as.listBox.selectedIdx = sel - 1; } else if (sel < 0) { w->as.listBox.selectedIdx = 0; } } else if (key == (0x47 | 0x100)) { // Home w->as.listBox.selectedIdx = 0; } else if (key == (0x4F | 0x100)) { // End w->as.listBox.selectedIdx = w->as.listBox.itemCount - 1; } else { return; } // Scroll to keep selection visible if (w->as.listBox.selectedIdx >= 0) { AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; const BitmapFontT *font = &ctx->font; int32_t innerH = w->h - LISTBOX_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; if (w->as.listBox.selectedIdx < w->as.listBox.scrollPos) { w->as.listBox.scrollPos = w->as.listBox.selectedIdx; } else if (w->as.listBox.selectedIdx >= w->as.listBox.scrollPos + visibleRows) { w->as.listBox.scrollPos = w->as.listBox.selectedIdx - visibleRows + 1; } } if (w->onChange) { w->onChange(w); } wgtInvalidate(w); } // ============================================================ // widgetListBoxOnMouse // ============================================================ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t innerH = hit->h - LISTBOX_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; bool needSb = (hit->as.listBox.itemCount > visibleRows); // Clamp scroll int32_t maxScroll = hit->as.listBox.itemCount - visibleRows; if (maxScroll < 0) { maxScroll = 0; } if (hit->as.listBox.scrollPos > maxScroll) { hit->as.listBox.scrollPos = maxScroll; } if (hit->as.listBox.scrollPos < 0) { hit->as.listBox.scrollPos = 0; } // Check if click is on the scrollbar if (needSb) { int32_t sbX = hit->x + hit->w - LISTBOX_BORDER - LISTBOX_SB_W; if (vx >= sbX) { int32_t sbY = hit->y + LISTBOX_BORDER; int32_t sbH = innerH; int32_t relY = vy - sbY; int32_t trackLen = sbH - LISTBOX_SB_W * 2; if (relY < LISTBOX_SB_W) { // Up arrow if (hit->as.listBox.scrollPos > 0) { hit->as.listBox.scrollPos--; } } else if (relY >= sbH - LISTBOX_SB_W) { // Down arrow if (hit->as.listBox.scrollPos < maxScroll) { hit->as.listBox.scrollPos++; } } else if (trackLen > 0) { // Track — page up/down int32_t thumbPos; int32_t thumbSize; listBoxScrollbarThumb(trackLen, hit->as.listBox.itemCount, visibleRows, hit->as.listBox.scrollPos, &thumbPos, &thumbSize); int32_t trackRelY = relY - LISTBOX_SB_W; if (trackRelY < thumbPos) { hit->as.listBox.scrollPos -= visibleRows; if (hit->as.listBox.scrollPos < 0) { hit->as.listBox.scrollPos = 0; } } else if (trackRelY >= thumbPos + thumbSize) { hit->as.listBox.scrollPos += visibleRows; if (hit->as.listBox.scrollPos > maxScroll) { hit->as.listBox.scrollPos = maxScroll; } } } hit->focused = true; return; } } // Click on item area int32_t innerY = hit->y + LISTBOX_BORDER; int32_t relY = vy - innerY; if (relY < 0) { return; } int32_t clickedRow = relY / font->charHeight; int32_t idx = hit->as.listBox.scrollPos + clickedRow; if (idx >= 0 && idx < hit->as.listBox.itemCount) { hit->as.listBox.selectedIdx = idx; hit->focused = true; if (hit->onChange) { hit->onChange(hit); } } } // ============================================================ // widgetListBoxPaint // ============================================================ void widgetListBoxPaint(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; int32_t innerH = w->h - LISTBOX_BORDER * 2; int32_t visibleRows = innerH / font->charHeight; bool needSb = (w->as.listBox.itemCount > visibleRows); int32_t contentW = w->w - LISTBOX_BORDER * 2; if (needSb) { contentW -= LISTBOX_SB_W; } // Sunken border BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = bg; bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Clamp scroll position int32_t maxScroll = w->as.listBox.itemCount - visibleRows; if (maxScroll < 0) { maxScroll = 0; } if (w->as.listBox.scrollPos > maxScroll) { w->as.listBox.scrollPos = maxScroll; } if (w->as.listBox.scrollPos < 0) { w->as.listBox.scrollPos = 0; } // Draw items int32_t innerX = w->x + LISTBOX_BORDER + LISTBOX_PAD; int32_t innerY = w->y + LISTBOX_BORDER; int32_t scrollPos = w->as.listBox.scrollPos; for (int32_t i = 0; i < visibleRows && (scrollPos + i) < w->as.listBox.itemCount; i++) { int32_t idx = scrollPos + i; int32_t iy = innerY + i * font->charHeight; uint32_t ifg = fg; uint32_t ibg = bg; if (idx == w->as.listBox.selectedIdx) { ifg = colors->menuHighlightFg; ibg = colors->menuHighlightBg; rectFill(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, ibg); } drawText(d, ops, font, innerX, iy, w->as.listBox.items[idx], ifg, ibg, idx == w->as.listBox.selectedIdx); } // Draw scrollbar if (needSb) { int32_t sbX = w->x + w->w - LISTBOX_BORDER - LISTBOX_SB_W; int32_t sbY = w->y + LISTBOX_BORDER; int32_t sbH = innerH; // Trough BevelStyleT troughBevel; troughBevel.highlight = colors->windowShadow; troughBevel.shadow = colors->windowHighlight; troughBevel.face = colors->scrollbarTrough; troughBevel.width = 1; drawBevel(d, ops, sbX, sbY, LISTBOX_SB_W, sbH, &troughBevel); // Up arrow button BevelStyleT btnBevel; btnBevel.highlight = colors->windowHighlight; btnBevel.shadow = colors->windowShadow; btnBevel.face = colors->windowFace; btnBevel.width = 1; drawBevel(d, ops, sbX, sbY, LISTBOX_SB_W, LISTBOX_SB_W, &btnBevel); // Up arrow triangle { int32_t cx = sbX + LISTBOX_SB_W / 2; int32_t cy = sbY + LISTBOX_SB_W / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg); } } // Down arrow button int32_t downY = sbY + sbH - LISTBOX_SB_W; drawBevel(d, ops, sbX, downY, LISTBOX_SB_W, LISTBOX_SB_W, &btnBevel); // Down arrow triangle { int32_t cx = sbX + LISTBOX_SB_W / 2; int32_t cy = downY + LISTBOX_SB_W / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg); } } // Thumb int32_t trackLen = sbH - LISTBOX_SB_W * 2; if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; listBoxScrollbarThumb(trackLen, w->as.listBox.itemCount, visibleRows, w->as.listBox.scrollPos, &thumbPos, &thumbSize); drawBevel(d, ops, sbX, sbY + LISTBOX_SB_W + thumbPos, LISTBOX_SB_W, thumbSize, &btnBevel); } } if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } }