From 236f6b4e397977f30891bd8b1028658651f7d784 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sun, 15 Mar 2026 22:32:55 -0500 Subject: [PATCH] Added ScrollPane. --- dvx/Makefile | 2 + dvx/dvxWidget.h | 14 +- dvx/widgets/widgetClass.c | 16 +- dvx/widgets/widgetInternal.h | 5 + dvx/widgets/widgetScrollPane.c | 617 +++++++++++++++++++++++++++++++++ dvxdemo/demo.c | 72 +++- 6 files changed, 706 insertions(+), 20 deletions(-) create mode 100644 dvx/widgets/widgetScrollPane.c diff --git a/dvx/Makefile b/dvx/Makefile index be167e9..ce20799 100644 --- a/dvx/Makefile +++ b/dvx/Makefile @@ -32,6 +32,7 @@ WSRCS = widgets/widgetAnsiTerm.c \ widgets/widgetListView.c \ widgets/widgetProgressBar.c \ widgets/widgetRadio.c \ + widgets/widgetScrollPane.c \ widgets/widgetSeparator.c \ widgets/widgetSlider.c \ widgets/widgetSpacer.c \ @@ -100,6 +101,7 @@ $(WOBJDIR)/widgetListBox.o: widgets/widgetListBox.c $(WIDGET_DEPS) $(WOBJDIR)/widgetListView.o: widgets/widgetListView.c $(WIDGET_DEPS) $(WOBJDIR)/widgetProgressBar.o: widgets/widgetProgressBar.c $(WIDGET_DEPS) $(WOBJDIR)/widgetRadio.o: widgets/widgetRadio.c $(WIDGET_DEPS) +$(WOBJDIR)/widgetScrollPane.o: widgets/widgetScrollPane.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSeparator.o: widgets/widgetSeparator.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSlider.o: widgets/widgetSlider.c $(WIDGET_DEPS) $(WOBJDIR)/widgetSpacer.o: widgets/widgetSpacer.c $(WIDGET_DEPS) diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 8c34d9f..645b5c3 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -69,7 +69,8 @@ typedef enum { WidgetCanvasE, WidgetAnsiTermE, WidgetListViewE, - WidgetSpinnerE + WidgetSpinnerE, + WidgetScrollPaneE } WidgetTypeE; // ============================================================ @@ -441,6 +442,11 @@ typedef struct WidgetT { int32_t undoCursor; bool editing; // true when user is typing } spinner; + + struct { + int32_t scrollPosV; + int32_t scrollPosH; + } scrollPane; } as; } WidgetT; @@ -577,6 +583,12 @@ void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected); void wgtListViewSelectAll(WidgetT *w); void wgtListViewClearSelection(WidgetT *w); +// ============================================================ +// ScrollPane +// ============================================================ + +WidgetT *wgtScrollPane(WidgetT *parent); + // ============================================================ // ImageButton // ============================================================ diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c index d6ed76d..fc54cce 100644 --- a/dvx/widgets/widgetClass.c +++ b/dvx/widgets/widgetClass.c @@ -370,6 +370,19 @@ static const WidgetClassT sClassAnsiTerm = { .setText = NULL }; +static const WidgetClassT sClassScrollPane = { + .flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE, + .paint = widgetScrollPanePaint, + .paintOverlay = NULL, + .calcMinSize = widgetScrollPaneCalcMinSize, + .layout = widgetScrollPaneLayout, + .onMouse = widgetScrollPaneOnMouse, + .onKey = widgetScrollPaneOnKey, + .destroy = NULL, + .getText = NULL, + .setText = NULL +}; + static const WidgetClassT sClassSpinner = { .flags = WCLASS_FOCUSABLE, .paint = widgetSpinnerPaint, @@ -416,5 +429,6 @@ const WidgetClassT *widgetClassTable[] = { [WidgetCanvasE] = &sClassCanvas, [WidgetAnsiTermE] = &sClassAnsiTerm, [WidgetListViewE] = &sClassListView, - [WidgetSpinnerE] = &sClassSpinner + [WidgetSpinnerE] = &sClassSpinner, + [WidgetScrollPaneE] = &sClassScrollPane }; diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 7cb52a5..01ccd27 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -170,6 +170,7 @@ bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy); 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); +void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -197,6 +198,7 @@ void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font); +void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font); @@ -209,6 +211,7 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font); // Per-widget layout functions (for special containers) // ============================================================ +void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font); void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font); void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font); @@ -277,6 +280,8 @@ void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetListViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod); +void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod); diff --git a/dvx/widgets/widgetScrollPane.c b/dvx/widgets/widgetScrollPane.c new file mode 100644 index 0000000..b1f9b62 --- /dev/null +++ b/dvx/widgets/widgetScrollPane.c @@ -0,0 +1,617 @@ +// widgetScrollPane.c — ScrollPane container widget + +#include "widgetInternal.h" + +#define SP_BORDER 2 +#define SP_SB_W 14 +#define SP_PAD 0 + + +// ============================================================ +// Prototypes +// ============================================================ + +static void drawSPHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW); +static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalH, int32_t visibleH); +static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb); + + +// ============================================================ +// drawSPHScrollbar +// ============================================================ + +static void drawSPHScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalW, int32_t visibleW) { + if (sbW < SP_SB_W * 3) { + return; + } + + uint32_t fg = colors->contentFg; + + // Trough + BevelStyleT troughBevel = BEVEL_TROUGH(colors); + drawBevel(d, ops, sbX, sbY, sbW, SP_SB_W, &troughBevel); + + // Left arrow button + BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); + drawBevel(d, ops, sbX, sbY, SP_SB_W, SP_SB_W, &btnBevel); + + { + int32_t cx = sbX + SP_SB_W / 2; + int32_t cy = sbY + SP_SB_W / 2; + + for (int32_t i = 0; i < 4; i++) { + drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, fg); + } + } + + // Right arrow button + int32_t rightX = sbX + sbW - SP_SB_W; + drawBevel(d, ops, rightX, sbY, SP_SB_W, SP_SB_W, &btnBevel); + + { + int32_t cx = rightX + SP_SB_W / 2; + int32_t cy = sbY + SP_SB_W / 2; + + for (int32_t i = 0; i < 4; i++) { + drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, fg); + } + } + + // Thumb + int32_t trackLen = sbW - SP_SB_W * 2; + + if (trackLen > 0 && totalW > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalW, visibleW, w->as.scrollPane.scrollPosH, &thumbPos, &thumbSize); + drawBevel(d, ops, sbX + SP_SB_W + thumbPos, sbY, thumbSize, SP_SB_W, &btnBevel); + } +} + + +// ============================================================ +// drawSPVScrollbar +// ============================================================ + +static void drawSPVScrollbar(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalH, int32_t visibleH) { + if (sbH < SP_SB_W * 3) { + return; + } + + uint32_t fg = colors->contentFg; + + // Trough + BevelStyleT troughBevel = BEVEL_TROUGH(colors); + drawBevel(d, ops, sbX, sbY, SP_SB_W, sbH, &troughBevel); + + // Up arrow button + BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); + drawBevel(d, ops, sbX, sbY, SP_SB_W, SP_SB_W, &btnBevel); + + { + int32_t cx = sbX + SP_SB_W / 2; + int32_t cy = sbY + SP_SB_W / 2; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, fg); + } + } + + // Down arrow button + int32_t downY = sbY + sbH - SP_SB_W; + drawBevel(d, ops, sbX, downY, SP_SB_W, SP_SB_W, &btnBevel); + + { + int32_t cx = sbX + SP_SB_W / 2; + int32_t cy = downY + SP_SB_W / 2; + + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, fg); + } + } + + // Thumb + int32_t trackLen = sbH - SP_SB_W * 2; + + if (trackLen > 0 && totalH > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalH, visibleH, w->as.scrollPane.scrollPosV, &thumbPos, &thumbSize); + drawBevel(d, ops, sbX, sbY + SP_SB_W + thumbPos, SP_SB_W, thumbSize, &btnBevel); + } +} + + +// ============================================================ +// spCalcNeeds — determine scrollbar needs and inner dimensions +// ============================================================ + +static void spCalcNeeds(WidgetT *w, const BitmapFontT *font, int32_t *contentMinW, int32_t *contentMinH, int32_t *innerW, int32_t *innerH, bool *needVSb, bool *needHSb) { + // Measure children + int32_t totalMinW = 0; + int32_t totalMinH = 0; + int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth); + int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); + int32_t count = 0; + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (!c->visible) { + continue; + } + + totalMinH += c->calcMinH; + totalMinW = DVX_MAX(totalMinW, c->calcMinW); + count++; + } + + if (count > 1) { + totalMinH += gap * (count - 1); + } + + totalMinW += pad * 2; + totalMinH += pad * 2; + + *contentMinW = totalMinW; + *contentMinH = totalMinH; + + // Available inner area + *innerW = w->w - SP_BORDER * 2; + *innerH = w->h - SP_BORDER * 2; + + // Determine scrollbar needs (two-pass for mutual dependency) + *needVSb = (totalMinH > *innerH); + *needHSb = false; + + if (*needVSb) { + *innerW -= SP_SB_W; + } + + if (totalMinW > *innerW) { + *needHSb = true; + *innerH -= SP_SB_W; + + if (!*needVSb && totalMinH > *innerH) { + *needVSb = true; + *innerW -= SP_SB_W; + } + } + + if (*innerW < 0) { + *innerW = 0; + } + + if (*innerH < 0) { + *innerH = 0; + } +} + + +// ============================================================ +// widgetScrollPaneCalcMinSize +// ============================================================ + +void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font) { + // Recursively measure children so they have valid calcMinW/H + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + widgetCalcMinSizeTree(c, font); + } + + // The scroll pane's own min size is small — the whole point is scrolling + w->calcMinW = SP_SB_W * 3 + SP_BORDER * 2; + w->calcMinH = SP_SB_W * 3 + SP_BORDER * 2; +} + + +// ============================================================ +// widgetScrollPaneLayout +// ============================================================ + +void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) { + int32_t contentMinW; + int32_t contentMinH; + int32_t innerW; + int32_t innerH; + bool needVSb; + bool needHSb; + + spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb); + + // Clamp scroll positions + int32_t maxScrollV = contentMinH - innerH; + int32_t maxScrollH = contentMinW - innerW; + + if (maxScrollV < 0) { + maxScrollV = 0; + } + + if (maxScrollH < 0) { + maxScrollH = 0; + } + + w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV); + w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH); + + // Layout children as a vertical box at virtual size + int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth); + int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); + int32_t virtualW = DVX_MAX(innerW, contentMinW); + int32_t virtualH = DVX_MAX(innerH, contentMinH); + int32_t baseX = w->x + SP_BORDER - w->as.scrollPane.scrollPosH; + int32_t baseY = w->y + SP_BORDER - w->as.scrollPane.scrollPosV; + int32_t childW = virtualW - pad * 2; + int32_t pos = baseY + pad; + + if (childW < 0) { + childW = 0; + } + + // Sum min sizes and weights for distribution + int32_t totalMin = 0; + int32_t totalWeight = 0; + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (!c->visible) { + continue; + } + + totalMin += c->calcMinH; + totalWeight += c->weight; + } + + int32_t count = widgetCountVisibleChildren(w); + int32_t totalGap = (count > 1) ? gap * (count - 1) : 0; + int32_t availMain = virtualH - pad * 2 - totalGap; + int32_t extraSpace = availMain - totalMin; + + if (extraSpace < 0) { + extraSpace = 0; + } + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (!c->visible) { + continue; + } + + int32_t mainSize = c->calcMinH; + + if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) { + mainSize += (extraSpace * c->weight) / totalWeight; + } + + c->x = baseX + pad; + c->y = pos; + c->w = childW; + c->h = mainSize; + + if (c->maxW) { + int32_t maxPx = wgtResolveSize(c->maxW, childW, font->charWidth); + + if (c->w > maxPx) { + c->w = maxPx; + } + } + + if (c->maxH) { + int32_t maxPx = wgtResolveSize(c->maxH, mainSize, font->charWidth); + + if (c->h > maxPx) { + c->h = maxPx; + } + } + + pos += mainSize + gap; + + // Recurse into child containers + widgetLayoutChildren(c, font); + } +} + + +// ============================================================ +// widgetScrollPaneOnKey +// ============================================================ + +void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + const BitmapFontT *font = &ctx->font; + + int32_t contentMinW; + int32_t contentMinH; + int32_t innerW; + int32_t innerH; + bool needVSb; + bool needHSb; + + spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb); + + int32_t maxScrollV = contentMinH - innerH; + int32_t maxScrollH = contentMinW - innerW; + + if (maxScrollV < 0) { + maxScrollV = 0; + } + + if (maxScrollH < 0) { + maxScrollH = 0; + } + + int32_t step = font->charHeight; + + if (key == (0x48 | 0x100)) { + // Up + w->as.scrollPane.scrollPosV -= step; + } else if (key == (0x50 | 0x100)) { + // Down + w->as.scrollPane.scrollPosV += step; + } else if (key == (0x49 | 0x100)) { + // Page Up + w->as.scrollPane.scrollPosV -= innerH; + } else if (key == (0x51 | 0x100)) { + // Page Down + w->as.scrollPane.scrollPosV += innerH; + } else if (key == (0x47 | 0x100)) { + // Home + w->as.scrollPane.scrollPosV = 0; + } else if (key == (0x4F | 0x100)) { + // End + w->as.scrollPane.scrollPosV = maxScrollV; + } else if (key == (0x4B | 0x100)) { + // Left + w->as.scrollPane.scrollPosH -= font->charWidth; + } else if (key == (0x4D | 0x100)) { + // Right + w->as.scrollPane.scrollPosH += font->charWidth; + } else { + return; + } + + w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV); + w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH); + wgtInvalidatePaint(w); +} + + +// ============================================================ +// widgetScrollPaneOnMouse +// ============================================================ + +void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { + AppContextT *ctx = (AppContextT *)root->userData; + const BitmapFontT *font = &ctx->font; + + int32_t contentMinW; + int32_t contentMinH; + int32_t innerW; + int32_t innerH; + bool needVSb; + bool needHSb; + + spCalcNeeds(hit, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb); + + // Clamp scroll positions + int32_t maxScrollV = contentMinH - innerH; + int32_t maxScrollH = contentMinW - innerW; + + if (maxScrollV < 0) { + maxScrollV = 0; + } + + if (maxScrollH < 0) { + maxScrollH = 0; + } + + hit->as.scrollPane.scrollPosV = clampInt(hit->as.scrollPane.scrollPosV, 0, maxScrollV); + hit->as.scrollPane.scrollPosH = clampInt(hit->as.scrollPane.scrollPosH, 0, maxScrollH); + + // Check vertical scrollbar + if (needVSb) { + int32_t sbX = hit->x + hit->w - SP_BORDER - SP_SB_W; + + if (vx >= sbX && vy >= hit->y + SP_BORDER && vy < hit->y + SP_BORDER + innerH) { + int32_t sbY = hit->y + SP_BORDER; + int32_t sbH = innerH; + int32_t relY = vy - sbY; + int32_t trackLen = sbH - SP_SB_W * 2; + int32_t pageSize = innerH - font->charHeight; + + if (pageSize < font->charHeight) { + pageSize = font->charHeight; + } + + if (relY < SP_SB_W) { + hit->as.scrollPane.scrollPosV -= font->charHeight; + } else if (relY >= sbH - SP_SB_W) { + hit->as.scrollPane.scrollPosV += font->charHeight; + } else if (trackLen > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, contentMinH, innerH, hit->as.scrollPane.scrollPosV, &thumbPos, &thumbSize); + + int32_t trackRelY = relY - SP_SB_W; + + if (trackRelY < thumbPos) { + hit->as.scrollPane.scrollPosV -= pageSize; + } else if (trackRelY >= thumbPos + thumbSize) { + hit->as.scrollPane.scrollPosV += pageSize; + } + } + + hit->as.scrollPane.scrollPosV = clampInt(hit->as.scrollPane.scrollPosV, 0, maxScrollV); + wgtInvalidatePaint(hit); + return; + } + } + + // Check horizontal scrollbar + if (needHSb) { + int32_t sbY = hit->y + hit->h - SP_BORDER - SP_SB_W; + + if (vy >= sbY && vx >= hit->x + SP_BORDER && vx < hit->x + SP_BORDER + innerW) { + int32_t sbX = hit->x + SP_BORDER; + int32_t sbW = innerW; + int32_t relX = vx - sbX; + int32_t trackLen = sbW - SP_SB_W * 2; + int32_t pageSize = innerW - font->charWidth; + + if (pageSize < font->charWidth) { + pageSize = font->charWidth; + } + + if (relX < SP_SB_W) { + hit->as.scrollPane.scrollPosH -= font->charWidth; + } else if (relX >= sbW - SP_SB_W) { + hit->as.scrollPane.scrollPosH += font->charWidth; + } else if (trackLen > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, contentMinW, innerW, hit->as.scrollPane.scrollPosH, &thumbPos, &thumbSize); + + int32_t trackRelX = relX - SP_SB_W; + + if (trackRelX < thumbPos) { + hit->as.scrollPane.scrollPosH -= pageSize; + } else if (trackRelX >= thumbPos + thumbSize) { + hit->as.scrollPane.scrollPosH += pageSize; + } + } + + hit->as.scrollPane.scrollPosH = clampInt(hit->as.scrollPane.scrollPosH, 0, maxScrollH); + wgtInvalidatePaint(hit); + return; + } + } + + // Dead corner + if (needVSb && needHSb) { + int32_t cornerX = hit->x + hit->w - SP_BORDER - SP_SB_W; + int32_t cornerY = hit->y + hit->h - SP_BORDER - SP_SB_W; + + if (vx >= cornerX && vy >= cornerY) { + return; + } + } + + // Click on content area — forward to child widgets + // Children are already positioned at scroll-adjusted coordinates + WidgetT *child = NULL; + + for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { + WidgetT *ch = widgetHitTest(c, vx, vy); + + if (ch) { + child = ch; + } + } + + if (child && child->enabled && child->wclass && child->wclass->onMouse) { + // Clear old focus + if (sFocusedWidget && sFocusedWidget != child) { + sFocusedWidget->focused = false; + } + + child->wclass->onMouse(child, root, vx, vy); + + if (child->focused) { + sFocusedWidget = child; + } + } else { + hit->focused = true; + } + + wgtInvalidatePaint(hit); +} + + +// ============================================================ +// widgetScrollPanePaint +// ============================================================ + +void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; + + int32_t contentMinW; + int32_t contentMinH; + int32_t innerW; + int32_t innerH; + bool needVSb; + bool needHSb; + + spCalcNeeds(w, font, &contentMinW, &contentMinH, &innerW, &innerH, &needVSb, &needHSb); + + // Clamp scroll + int32_t maxScrollV = contentMinH - innerH; + int32_t maxScrollH = contentMinW - innerW; + + if (maxScrollV < 0) { + maxScrollV = 0; + } + + if (maxScrollH < 0) { + maxScrollH = 0; + } + + w->as.scrollPane.scrollPosV = clampInt(w->as.scrollPane.scrollPosV, 0, maxScrollV); + w->as.scrollPane.scrollPosH = clampInt(w->as.scrollPane.scrollPosH, 0, maxScrollH); + + // Sunken border + BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, SP_BORDER); + drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); + + // Clip to content area and paint children + int32_t oldClipX = d->clipX; + int32_t oldClipY = d->clipY; + int32_t oldClipW = d->clipW; + int32_t oldClipH = d->clipH; + setClipRect(d, w->x + SP_BORDER, w->y + SP_BORDER, innerW, innerH); + + // Fill background + rectFill(d, ops, w->x + SP_BORDER, w->y + SP_BORDER, innerW, innerH, bg); + + // Paint children (already positioned by layout with scroll offset) + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + widgetPaintOne(c, d, ops, font, colors); + } + + setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); + + // Draw scrollbars + if (needVSb) { + int32_t sbX = w->x + w->w - SP_BORDER - SP_SB_W; + int32_t sbY = w->y + SP_BORDER; + drawSPVScrollbar(w, d, ops, colors, sbX, sbY, innerH, contentMinH, innerH); + } + + if (needHSb) { + int32_t sbX = w->x + SP_BORDER; + int32_t sbY = w->y + w->h - SP_BORDER - SP_SB_W; + drawSPHScrollbar(w, d, ops, colors, sbX, sbY, innerW, contentMinW, innerW); + + if (needVSb) { + rectFill(d, ops, sbX + innerW, sbY, SP_SB_W, SP_SB_W, colors->windowFace); + } + } + + if (w->focused) { + uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); + } +} + + +// ============================================================ +// wgtScrollPane +// ============================================================ + +WidgetT *wgtScrollPane(WidgetT *parent) { + WidgetT *w = widgetAlloc(parent, WidgetScrollPaneE); + + if (w) { + w->as.scrollPane.scrollPosV = 0; + w->as.scrollPane.scrollPosH = 0; + w->weight = 100; + } + + return w; +} diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index be522bf..7b888be 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -481,10 +481,46 @@ static void setupControlsWindow(AppContextT *ctx) { wgtListViewSetMultiSelect(lv, true); lv->weight = 100; - // --- Tab 4: Toolbar (ImageButtons + VSeparator) --- - WidgetT *page4 = wgtTabPage(tabs, "Tool&bar"); + // --- Tab 4: ScrollPane --- + WidgetT *page4sp = wgtTabPage(tabs, "&Scroll"); - WidgetT *tb = wgtToolbar(page4); + WidgetT *sp = wgtScrollPane(page4sp); + sp->weight = 100; + sp->padding = wgtPixels(4); + sp->spacing = wgtPixels(4); + + wgtLabel(sp, "ScrollPane Demo:"); + wgtHSeparator(sp); + + static const char *spItems[] = { + "Item &1", "Item &2", "Item &3", "Item &4", + "Item &5", "Item &6", "Item &7", "Item &8" + }; + + for (int32_t i = 0; i < 8; i++) { + wgtCheckbox(sp, spItems[i]); + } + + wgtHSeparator(sp); + wgtLabel(sp, "More widgets below:"); + + WidgetT *spRg = wgtRadioGroup(sp); + static const char *spRadios[] = { + "Radio 1", "Radio 2", "Radio 3", "Radio 4", "Radio 5" + }; + + for (int32_t i = 0; i < 5; i++) { + wgtRadio(spRg, spRadios[i]); + } + + wgtHSeparator(sp); + wgtLabel(sp, "Bottom of scroll area"); + wgtButton(sp, "Scrolled &Button"); + + // --- Tab 5: Toolbar (ImageButtons + VSeparator) --- + WidgetT *page5tb = wgtTabPage(tabs, "Tool&bar"); + + WidgetT *tb = wgtToolbar(page5tb); int32_t imgW; int32_t imgH; @@ -515,18 +551,18 @@ static void setupControlsWindow(AppContextT *ctx) { WidgetT *btnHelp = wgtButton(tb, "&Help"); btnHelp->onClick = onToolbarClick; - wgtLabel(page4, "ImageButtons with VSeparator."); + wgtLabel(page5tb, "ImageButtons with VSeparator."); - // --- Tab 5: Media (Image, ImageFromFile) --- - WidgetT *page5 = wgtTabPage(tabs, "&Media"); + // --- Tab 6: Media (Image, ImageFromFile) --- + WidgetT *page6m = wgtTabPage(tabs, "&Media"); - wgtLabel(page5, "ImageFromFile (sample.bmp):"); - wgtImageFromFile(page5, "sample.bmp"); + wgtLabel(page6m, "ImageFromFile (sample.bmp):"); + wgtImageFromFile(page6m, "sample.bmp"); - wgtHSeparator(page5); + wgtHSeparator(page6m); - wgtLabel(page5, "Image (logo.bmp):"); - WidgetT *imgRow = wgtHBox(page5); + wgtLabel(page6m, "Image (logo.bmp):"); + WidgetT *imgRow = wgtHBox(page6m); uint8_t *logoData = loadBmpPixels(ctx, "logo.bmp", &imgW, &imgH, &imgPitch); if (logoData) { wgtImage(imgRow, logoData, imgW, imgH, imgPitch); @@ -534,19 +570,19 @@ static void setupControlsWindow(AppContextT *ctx) { wgtVSeparator(imgRow); wgtLabel(imgRow, "32x32 DV/X logo"); - // --- Tab 6: Editor (TextArea, Canvas) --- - WidgetT *page6 = wgtTabPage(tabs, "&Editor"); + // --- Tab 7: Editor (TextArea, Canvas) --- + WidgetT *page7e = wgtTabPage(tabs, "&Editor"); - wgtLabel(page6, "TextArea:"); - WidgetT *ta = wgtTextArea(page6, 512); + wgtLabel(page7e, "TextArea:"); + WidgetT *ta = wgtTextArea(page7e, 512); ta->weight = 100; wgtSetText(ta, "Multi-line text editor.\n\nFeatures:\n- Word wrap\n- Selection\n- Copy/Paste\n- Undo (Ctrl+Z)"); - wgtHSeparator(page6); + wgtHSeparator(page7e); - wgtLabel(page6, "Canvas (draw with mouse):"); + wgtLabel(page7e, "Canvas (draw with mouse):"); const DisplayT *d = dvxGetDisplay(ctx); - WidgetT *cv = wgtCanvas(page6, 280, 80); + WidgetT *cv = wgtCanvas(page7e, 280, 80); wgtCanvasSetPenColor(cv, packColor(d, 200, 0, 0)); wgtCanvasDrawRect(cv, 5, 5, 50, 35); wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 200));