// widgetListBox.c — ListBox widget (single and multi-select) #include "widgetInternal.h" #include #include #define LISTBOX_BORDER 2 #define LISTBOX_PAD 2 #define LISTBOX_MIN_ROWS 4 #define LISTBOX_SB_W 14 // ============================================================ // Prototypes // ============================================================ static void allocSelBits(WidgetT *w); static void ensureScrollVisible(WidgetT *w, int32_t idx); static void selectRange(WidgetT *w, int32_t from, int32_t to); // ============================================================ // allocSelBits // ============================================================ static void allocSelBits(WidgetT *w) { if (w->as.listBox.selBits) { free(w->as.listBox.selBits); w->as.listBox.selBits = NULL; } int32_t count = w->as.listBox.itemCount; if (count > 0 && w->as.listBox.multiSelect) { w->as.listBox.selBits = (uint8_t *)calloc(count, 1); } } // ============================================================ // ensureScrollVisible // ============================================================ static void ensureScrollVisible(WidgetT *w, int32_t idx) { if (idx < 0) { return; } 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 (visibleRows < 1) { visibleRows = 1; } if (idx < w->as.listBox.scrollPos) { w->as.listBox.scrollPos = idx; } else if (idx >= w->as.listBox.scrollPos + visibleRows) { w->as.listBox.scrollPos = idx - visibleRows + 1; } } // ============================================================ // selectRange // ============================================================ static void selectRange(WidgetT *w, int32_t from, int32_t to) { if (!w->as.listBox.selBits) { return; } int32_t lo = from < to ? from : to; int32_t hi = from > to ? from : to; if (lo < 0) { lo = 0; } if (hi >= w->as.listBox.itemCount) { hi = w->as.listBox.itemCount - 1; } for (int32_t i = lo; i <= hi; i++) { w->as.listBox.selBits[i] = 1; } } // ============================================================ // widgetListBoxDestroy // ============================================================ void widgetListBoxDestroy(WidgetT *w) { if (w->as.listBox.selBits) { free(w->as.listBox.selBits); w->as.listBox.selBits = NULL; } } // ============================================================ // wgtListBox // ============================================================ WidgetT *wgtListBox(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, WidgetListBoxE); if (w) { w->as.listBox.selectedIdx = -1; w->as.listBox.anchorIdx = -1; } return w; } // ============================================================ // wgtListBoxClearSelection // ============================================================ void wgtListBoxClearSelection(WidgetT *w) { if (!w || w->type != WidgetListBoxE || !w->as.listBox.selBits) { return; } memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount); } // ============================================================ // wgtListBoxGetSelected // ============================================================ int32_t wgtListBoxGetSelected(const WidgetT *w) { if (!w || w->type != WidgetListBoxE) { return -1; } return w->as.listBox.selectedIdx; } // ============================================================ // wgtListBoxIsItemSelected // ============================================================ bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx) { if (!w || w->type != WidgetListBoxE) { return false; } if (!w->as.listBox.multiSelect) { return idx == w->as.listBox.selectedIdx; } if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) { return false; } return w->as.listBox.selBits[idx] != 0; } // ============================================================ // wgtListBoxSelectAll // ============================================================ void wgtListBoxSelectAll(WidgetT *w) { if (!w || w->type != WidgetListBoxE || !w->as.listBox.selBits) { return; } memset(w->as.listBox.selBits, 1, w->as.listBox.itemCount); } // ============================================================ // wgtListBoxSetItemSelected // ============================================================ void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected) { if (!w || w->type != WidgetListBoxE) { return; } if (!w->as.listBox.selBits || idx < 0 || idx >= w->as.listBox.itemCount) { return; } w->as.listBox.selBits[idx] = selected ? 1 : 0; } // ============================================================ // 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; // 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.listBox.maxItemLen = maxLen; 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; } w->as.listBox.anchorIdx = w->as.listBox.selectedIdx; // Reallocate selection bits allocSelBits(w); // Pre-select the cursor item if (w->as.listBox.selBits && w->as.listBox.selectedIdx >= 0) { w->as.listBox.selBits[w->as.listBox.selectedIdx] = 1; } } // ============================================================ // wgtListBoxSetMultiSelect // ============================================================ void wgtListBoxSetMultiSelect(WidgetT *w, bool multi) { if (!w || w->type != WidgetListBoxE) { return; } w->as.listBox.multiSelect = multi; allocSelBits(w); // Sync: mark current selection if (w->as.listBox.selBits && w->as.listBox.selectedIdx >= 0) { w->as.listBox.selBits[w->as.listBox.selectedIdx] = 1; } } // ============================================================ // wgtListBoxSetSelected // ============================================================ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) { if (!w || w->type != WidgetListBoxE) { return; } w->as.listBox.selectedIdx = idx; w->as.listBox.anchorIdx = idx; // In multi-select, clear all then select just this one if (w->as.listBox.selBits) { memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount); if (idx >= 0 && idx < w->as.listBox.itemCount) { w->as.listBox.selBits[idx] = 1; } } } // ============================================================ // widgetListBoxCalcMinSize // ============================================================ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { int32_t maxItemW = w->as.listBox.maxItemLen * font->charWidth; int32_t minW = font->charWidth * 8; if (maxItemW < minW) { maxItemW = minW; } 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, int32_t mod) { if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) { return; } bool multi = w->as.listBox.multiSelect; bool shift = (mod & KEY_MOD_SHIFT) != 0; bool ctrl = (mod & KEY_MOD_CTRL) != 0; int32_t sel = w->as.listBox.selectedIdx; // Ctrl+A — select all (multi-select only) if (multi && ctrl && (key == 'a' || key == 'A' || key == 1)) { wgtListBoxSelectAll(w); wgtInvalidatePaint(w); return; } // Space — toggle current item (multi-select only) if (multi && key == ' ') { if (sel >= 0 && w->as.listBox.selBits) { w->as.listBox.selBits[sel] ^= 1; w->as.listBox.anchorIdx = sel; } if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(w); return; } int32_t newSel = sel; if (key == (0x50 | 0x100)) { // Down arrow if (newSel < w->as.listBox.itemCount - 1) { newSel++; } else if (newSel < 0) { newSel = 0; } } else if (key == (0x48 | 0x100)) { // Up arrow if (newSel > 0) { newSel--; } else if (newSel < 0) { newSel = 0; } } else if (key == (0x47 | 0x100)) { // Home newSel = 0; } else if (key == (0x4F | 0x100)) { // End newSel = w->as.listBox.itemCount - 1; } else if (key == (0x51 | 0x100)) { // Page Down AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; const BitmapFontT *font = &ctx->font; int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; if (visibleRows < 1) { visibleRows = 1; } newSel += visibleRows; if (newSel >= w->as.listBox.itemCount) { newSel = w->as.listBox.itemCount - 1; } } else if (key == (0x49 | 0x100)) { // Page Up AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; const BitmapFontT *font = &ctx->font; int32_t visibleRows = (w->h - LISTBOX_BORDER * 2) / font->charHeight; if (visibleRows < 1) { visibleRows = 1; } newSel -= visibleRows; if (newSel < 0) { newSel = 0; } } else { return; } if (newSel == sel) { return; } w->as.listBox.selectedIdx = newSel; // Update selection if (multi && w->as.listBox.selBits) { if (shift) { // Shift+arrow: range from anchor to new cursor memset(w->as.listBox.selBits, 0, w->as.listBox.itemCount); selectRange(w, w->as.listBox.anchorIdx, newSel); } // Plain arrow: just move cursor, leave selections untouched } ensureScrollVisible(w, newSel); if (w->onChange) { w->onChange(w); } wgtInvalidatePaint(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; } hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll); // 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; widgetScrollbarThumb(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; hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll); } else if (trackRelY >= thumbPos + thumbSize) { hit->as.listBox.scrollPos += visibleRows; hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, 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) { return; } bool multi = hit->as.listBox.multiSelect; bool shift = (ctx->keyModifiers & KEY_MOD_SHIFT) != 0; bool ctrl = (ctx->keyModifiers & KEY_MOD_CTRL) != 0; hit->as.listBox.selectedIdx = idx; hit->focused = true; if (multi && hit->as.listBox.selBits) { if (ctrl) { // Ctrl+click: toggle item, update anchor hit->as.listBox.selBits[idx] ^= 1; hit->as.listBox.anchorIdx = idx; } else if (shift) { // Shift+click: range from anchor to clicked memset(hit->as.listBox.selBits, 0, hit->as.listBox.itemCount); selectRange(hit, hit->as.listBox.anchorIdx, idx); } else { // Plain click: select only this item, update anchor memset(hit->as.listBox.selBits, 0, hit->as.listBox.itemCount); hit->as.listBox.selBits[idx] = 1; hit->as.listBox.anchorIdx = idx; } } if (hit->onChange) { hit->onChange(hit); } if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { hit->onDblClick(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_SUNKEN(colors, bg, 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; } w->as.listBox.scrollPos = clampInt(w->as.listBox.scrollPos, 0, maxScroll); // 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; bool multi = w->as.listBox.multiSelect; uint8_t *selBits = w->as.listBox.selBits; 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; bool isSelected; if (multi && selBits) { isSelected = selBits[idx] != 0; } else { isSelected = (idx == w->as.listBox.selectedIdx); } if (isSelected) { 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, false); // Draw cursor indicator in multi-select (dotted focus rect on cursor item) if (multi && idx == w->as.listBox.selectedIdx && w->focused) { drawFocusRect(d, ops, w->x + LISTBOX_BORDER, iy, contentW, font->charHeight, fg); } } // 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 = BEVEL_TROUGH(colors); drawBevel(d, ops, sbX, sbY, LISTBOX_SB_W, sbH, &troughBevel); // Up arrow button BevelStyleT btnBevel = BEVEL_RAISED(colors, 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; widgetScrollbarThumb(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); } }