// widgetScrollPane.c — ScrollPane container widget // // A clipping container that allows its children (laid out as a // vertical box) to overflow and be scrolled into view. This is the // general-purpose scrollable container -- unlike TreeView or ListBox // which have item-specific scrolling, ScrollPane can wrap any // collection of arbitrary child widgets. // // Architecture: ScrollPane is both a layout container and a paint // container (WCLASS_PAINTS_CHILDREN flag). It lays out children at // their virtual positions (offset by scroll position), then during // paint, sets a clip rect to the inner content area before painting // children. This means children are positioned at coordinates that // may be outside the visible area -- the clip rect handles hiding // the overflow. This is simpler and more efficient than per-widget // visibility culling for the small widget counts typical on 486 DOS. // // Scrollbar visibility uses a two-pass determination: first check if // V scrollbar is needed, then check H (accounting for the space the // V scrollbar consumed), then re-check V in case H scrollbar's // appearance reduced available height. This handles the mutual // dependency where adding one scrollbar may trigger the other. // // The scroll pane has its own copies of the scrollbar drawing routines // (drawSPHScrollbar, drawSPVScrollbar) rather than using the shared // widgetDrawScrollbarH/V because it uses its own SP_SB_W constant. // This is a minor duplication tradeoff for allowing different scrollbar // widths in different contexts. #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 // ============================================================ // Central sizing function called by layout, paint, and mouse handlers. // Computes the total content min size from children, then determines // which scrollbars are needed and adjusts inner dimensions accordingly. // The two-pass scrollbar dependency resolution handles the case where // adding a V scrollbar shrinks the width enough to need an H scrollbar, // which in turn shrinks the height enough to need a V scrollbar. 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 // ============================================================ // The scroll pane reports a deliberately small min size (just enough // for the scrollbar chrome) because its whole purpose is to contain // content that doesn't fit. However, children still need their min // sizes computed so spCalcNeeds can determine scrollbar visibility // and the layout pass can distribute space correctly. 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 // ============================================================ // Layout is a vertical box layout offset by the scroll position. // Children are positioned at their "virtual" coordinates (baseX/baseY // incorporate the negative scroll offset), so they may have negative // or very large Y values. The paint pass clips to the visible area. // This means child coordinates are always absolute screen coords, // keeping the draw path simple -- no coordinate translation needed // at paint time. // // Extra space distribution uses the same weight-based algorithm as // the generic box layout: each child gets a share of surplus space // proportional to its weight/totalWeight ratio. This allows stretch // children inside the scrollable area. 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 // ============================================================ // Keyboard scrolling uses font metrics for step sizes: charHeight for // vertical line scroll, charWidth for horizontal, and the full inner // height for page scroll. This makes scroll distance proportional to // content size, which feels natural. The early return for unhandled // keys avoids unnecessary invalidation. void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)mod; AppContextT *ctx = wgtGetContext(w); 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 // ============================================================ // Mouse handling has priority order: V scrollbar > H scrollbar > dead // corner > child content. The dead corner (where H and V scrollbars // meet) is explicitly handled to prevent clicks from falling through // to content behind it. Content clicks do recursive hit-testing into // children and forward the mouse event, handling focus management // along the way. This is necessary because scroll pane has // WCLASS_NO_HIT_RECURSE -- the generic hit-test doesn't descend // into scroll pane children since their coordinates may be outside // the pane's visible bounds. 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; } else { sDragScrollbar = hit; sDragScrollbarOrient = 0; sDragScrollbarOff = trackRelY - thumbPos; return; } } 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; } else { sDragScrollbar = hit; sDragScrollbarOrient = 1; sDragScrollbarOff = trackRelX - thumbPos; return; } } 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 // ============================================================ // Paint saves and restores the clip rect around child painting. // Children are painted with a clip rect that excludes the scrollbar // area, so children that extend past the visible content area are // automatically clipped. Scrollbars are painted after restoring the // clip rect so they're always fully visible. The dead corner (when // both scrollbars are present) is filled with windowFace color. 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 // ============================================================ // Default weight=100 so the scroll pane stretches to fill available // space in its parent container. Without this, a scroll pane in a // vertical box would collapse to its minimal size. 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; }