682 lines
20 KiB
C
682 lines
20 KiB
C
// widgetListBox.c — ListBox widget (single and multi-select)
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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;
|
|
w->as.listBox.dragIdx = -1;
|
|
w->as.listBox.dropIdx = -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;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtListBoxSetReorderable
|
|
// ============================================================
|
|
|
|
void wgtListBoxSetReorderable(WidgetT *w, bool reorderable) {
|
|
if (!w || w->type != WidgetListBoxE) {
|
|
return;
|
|
}
|
|
|
|
w->as.listBox.reorderable = reorderable;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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);
|
|
}
|
|
|
|
// Initiate drag-reorder if enabled (not from modifier clicks)
|
|
if (hit->as.listBox.reorderable && !shift && !ctrl) {
|
|
hit->as.listBox.dragIdx = idx;
|
|
hit->as.listBox.dropIdx = idx;
|
|
sDragReorder = 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 drag-reorder insertion indicator
|
|
if (w->as.listBox.reorderable && w->as.listBox.dragIdx >= 0 && w->as.listBox.dropIdx >= 0) {
|
|
int32_t drop = w->as.listBox.dropIdx;
|
|
int32_t lineY = innerY + (drop - scrollPos) * font->charHeight;
|
|
|
|
if (lineY >= innerY && lineY <= innerY + innerH) {
|
|
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY, contentW, colors->contentFg);
|
|
drawHLine(d, ops, w->x + LISTBOX_BORDER, lineY + 1, contentW, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|