diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index f2b0c22..25876cf 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -121,7 +121,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { while (top > 0) { WidgetT *w = stack[--top]; - if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetDropdownE || w->type == WidgetAnsiTermE || w->type == WidgetTreeViewE)) { + if (w->focused && (w->type == WidgetTextInputE || w->type == WidgetComboBoxE || w->type == WidgetDropdownE || w->type == WidgetAnsiTermE || w->type == WidgetTreeViewE || w->type == WidgetListBoxE)) { focus = w; break; } @@ -150,6 +150,12 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { return; } + // Handle list box keyboard navigation + if (focus->type == WidgetListBoxE) { + widgetListBoxOnKey(focus, key); + return; + } + // Handle dropdown keyboard navigation if (focus->type == WidgetDropdownE) { if (focus->as.dropdown.open) { @@ -672,6 +678,10 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { widgetCanvasOnMouse(hit, vx, vy); } + if (hit->type == WidgetListBoxE && hit->enabled) { + widgetListBoxOnMouse(hit, root, vx, vy); + } + if (hit->type == WidgetDropdownE && hit->enabled) { if (hit != closedPopup) { widgetDropdownOnMouse(hit); diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 966d09f..6d83347 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -113,6 +113,7 @@ void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetImagePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -136,6 +137,7 @@ void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font); +void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font); @@ -164,6 +166,8 @@ void widgetCheckboxOnMouse(WidgetT *hit); void widgetComboBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx); void widgetDropdownOnMouse(WidgetT *hit); void widgetImageOnMouse(WidgetT *hit); +void widgetListBoxOnKey(WidgetT *w, int32_t key); +void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); void widgetRadioOnMouse(WidgetT *hit); void widgetSliderOnMouse(WidgetT *hit, int32_t vx, int32_t vy); void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy); diff --git a/dvx/widgets/widgetLayout.c b/dvx/widgets/widgetLayout.c index 225c06a..e1f8336 100644 --- a/dvx/widgets/widgetLayout.c +++ b/dvx/widgets/widgetLayout.c @@ -121,6 +121,9 @@ void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) { case WidgetImageE: widgetImageCalcMinSize(w, font); break; + case WidgetListBoxE: + widgetListBoxCalcMinSize(w, font); + break; case WidgetImageButtonE: widgetImageButtonCalcMinSize(w, font); break; diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index fc95a77..85d0cb1 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -2,6 +2,43 @@ #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 @@ -44,7 +81,11 @@ void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count) { w->as.listBox.itemCount = count; if (w->as.listBox.selectedIdx >= count) { - w->as.listBox.selectedIdx = -1; + w->as.listBox.selectedIdx = count > 0 ? 0 : -1; + } + + if (w->as.listBox.selectedIdx < 0 && count > 0) { + w->as.listBox.selectedIdx = 0; } } @@ -60,3 +101,297 @@ void wgtListBoxSetSelected(WidgetT *w, int32_t idx) { 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); + } + } +} diff --git a/dvx/widgets/widgetOps.c b/dvx/widgets/widgetOps.c index 4d7cf79..5eff737 100644 --- a/dvx/widgets/widgetOps.c +++ b/dvx/widgets/widgetOps.c @@ -107,6 +107,10 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo widgetComboBoxPaint(w, d, ops, font, colors); break; + case WidgetListBoxE: + widgetListBoxPaint(w, d, ops, font, colors); + break; + case WidgetProgressBarE: widgetProgressBarPaint(w, d, ops, font, colors); break;