// 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; }