#define DVX_WIDGET_IMPL // 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 "dvxWidgetPlugin.h" static int32_t sTypeId = -1; typedef struct { int32_t scrollPosV; int32_t scrollPosH; int32_t sbDragOrient; int32_t sbDragOff; bool noBorder; } ScrollPaneDataT; #define SP_BORDER 2 #define SP_SB_W 14 #define SP_PAD 0 static int32_t spBorder(const WidgetT *w) { const ScrollPaneDataT *sp = (const ScrollPaneDataT *)w->data; return sp->noBorder ? 0 : SP_BORDER; } // ============================================================ // 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); static void widgetScrollPaneDestroy(WidgetT *w); static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY); void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child); // ============================================================ // 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; } ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; 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, sp->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; } ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; 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, sp->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 - spBorder(w) * 2; *innerH = w->h - spBorder(w) * 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 + spBorder(w) * 2; w->calcMinH = SP_SB_W * 3 + spBorder(w) * 2; } // ============================================================ // widgetScrollPaneDestroy // ============================================================ static void widgetScrollPaneDestroy(WidgetT *w) { free(w->data); w->data = NULL; } // ============================================================ // 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) { ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; 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; } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosH = clampInt(sp->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 + spBorder(w) - sp->scrollPosH; int32_t baseY = w->y + spBorder(w) - sp->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); } // Children with custom layout (e.g., WrapBox) may have updated // their calcMinH after layout. Re-check if scrollbars are needed. { int32_t newMinW2; int32_t newMinH2; int32_t newInnerW2; int32_t newInnerH2; bool newNeedV2; bool newNeedH2; spCalcNeeds(w, font, &newMinW2, &newMinH2, &newInnerW2, &newInnerH2, &newNeedV2, &newNeedH2); if (newNeedV2 != needVSb || newNeedH2 != needHSb) { // Scrollbar needs changed — redo layout with updated sizes contentMinH = newMinH2; contentMinW = newMinW2; innerW = newInnerW2; innerH = newInnerH2; maxScrollV = contentMinH - innerH; maxScrollH = contentMinW - innerW; if (maxScrollV < 0) { maxScrollV = 0; } if (maxScrollH < 0) { maxScrollH = 0; } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); virtualW = DVX_MAX(innerW, contentMinW); virtualH = DVX_MAX(innerH, contentMinH); baseX = w->x + spBorder(w) - sp->scrollPosH; baseY = w->y + spBorder(w) - sp->scrollPosV; childW = virtualW - pad * 2; if (childW < 0) { childW = 0; } pos = baseY + pad; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (!c->visible) { continue; } int32_t ms = c->calcMinH; if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) { ms += (extraSpace * c->weight) / totalWeight; } c->x = baseX + pad; c->y = pos; c->w = childW; c->h = ms; pos += ms + gap; 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; ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; 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 sp->scrollPosV -= step; } else if (key == (0x50 | 0x100)) { // Down sp->scrollPosV += step; } else if (key == (0x49 | 0x100)) { // Page Up sp->scrollPosV -= innerH; } else if (key == (0x51 | 0x100)) { // Page Down sp->scrollPosV += innerH; } else if (key == (0x47 | 0x100)) { // Home sp->scrollPosV = 0; } else if (key == (0x4F | 0x100)) { // End sp->scrollPosV = maxScrollV; } else if (key == (0x4B | 0x100)) { // Left sp->scrollPosH -= font->charWidth; } else if (key == (0x4D | 0x100)) { // Right sp->scrollPosH += font->charWidth; } else { return; } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); wgtInvalidate(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) { const WidgetT *w = hit; // alias for spBorder() ScrollPaneDataT *sp = (ScrollPaneDataT *)hit->data; 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; } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); // Check vertical scrollbar if (needVSb) { int32_t sbX = hit->x + hit->w - spBorder(w) - SP_SB_W; if (vx >= sbX && vy >= hit->y + spBorder(w) && vy < hit->y + spBorder(w) + innerH) { int32_t sbY = hit->y + spBorder(w); 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) { sp->scrollPosV -= font->charHeight; } else if (relY >= sbH - SP_SB_W) { sp->scrollPosV += font->charHeight; } else if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &thumbSize); int32_t trackRelY = relY - SP_SB_W; if (trackRelY < thumbPos) { sp->scrollPosV -= pageSize; } else if (trackRelY >= thumbPos + thumbSize) { sp->scrollPosV += pageSize; } else { sDragWidget = hit; sp->sbDragOrient = 0; sp->sbDragOff = trackRelY - thumbPos; return; } } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); wgtInvalidate(hit); return; } } // Check horizontal scrollbar if (needHSb) { int32_t sbY = hit->y + hit->h - spBorder(w) - SP_SB_W; if (vy >= sbY && vx >= hit->x + spBorder(w) && vx < hit->x + spBorder(w) + innerW) { int32_t sbX = hit->x + spBorder(w); 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) { sp->scrollPosH -= font->charWidth; } else if (relX >= sbW - SP_SB_W) { sp->scrollPosH += font->charWidth; } else if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize); int32_t trackRelX = relX - SP_SB_W; if (trackRelX < thumbPos) { sp->scrollPosH -= pageSize; } else if (trackRelX >= thumbPos + thumbSize) { sp->scrollPosH += pageSize; } else { sDragWidget = hit; sp->sbDragOrient = 1; sp->sbDragOff = trackRelX - thumbPos; return; } } sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); wgtInvalidate(hit); return; } } // Dead corner if (needVSb && needHSb) { int32_t cornerX = hit->x + hit->w - spBorder(w) - SP_SB_W; int32_t cornerY = hit->y + hit->h - spBorder(w) - 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 && wclsHas(child, WGT_METHOD_ON_MOUSE)) { wclsOnMouse(child, root, vx, vy); } else { sFocusedWidget = hit; } wgtInvalidatePaint(hit); } // ============================================================ // widgetScrollPaneOnDragUpdate // ============================================================ // Handle scrollbar thumb drag for vertical and horizontal scrollbars. // Uses spCalcNeeds to determine content and viewport dimensions. static void widgetScrollPaneOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY) { (void)root; ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; int32_t orient = sp->sbDragOrient; int32_t dragOff = sp->sbDragOff; 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); if (orient == 0) { // Vertical scrollbar drag int32_t maxScroll = contentMinH - innerH; if (maxScroll <= 0) { return; } int32_t trackLen = innerH - SP_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, contentMinH, innerH, sp->scrollPosV, &thumbPos, &thumbSize); int32_t sbY = w->y + spBorder(w); int32_t relMouse = mouseY - sbY - SP_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; sp->scrollPosV = clampInt(newScroll, 0, maxScroll); } else if (orient == 1) { // Horizontal scrollbar drag int32_t maxScroll = contentMinW - innerW; if (maxScroll <= 0) { return; } int32_t trackLen = innerW - SP_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, contentMinW, innerW, sp->scrollPosH, &thumbPos, &thumbSize); int32_t sbX = w->x + spBorder(w); int32_t relMouse = mouseX - sbX - SP_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; sp->scrollPosH = clampInt(newScroll, 0, maxScroll); } } // ============================================================ // 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) { ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; 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; } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH); // Sunken border BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, spBorder(w)); 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 + spBorder(w), w->y + spBorder(w), innerW, innerH); // Fill background rectFill(d, ops, w->x + spBorder(w), w->y + spBorder(w), 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 - spBorder(w) - SP_SB_W; int32_t sbY = w->y + spBorder(w); drawSPVScrollbar(w, d, ops, colors, sbX, sbY, innerH, contentMinH, innerH); } if (needHSb) { int32_t sbX = w->x + spBorder(w); int32_t sbY = w->y + w->h - spBorder(w) - 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 == sFocusedWidget) { 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); } } // ============================================================ // DXE registration // ============================================================ static const WidgetClassT sClassScrollPane = { .version = WGT_CLASS_VERSION, .flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLL_CONTAINER | WCLASS_RELAYOUT_ON_SCROLL, .handlers = { [WGT_METHOD_PAINT] = (void *)widgetScrollPanePaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetScrollPaneCalcMinSize, [WGT_METHOD_LAYOUT] = (void *)widgetScrollPaneLayout, [WGT_METHOD_ON_MOUSE] = (void *)widgetScrollPaneOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetScrollPaneOnKey, [WGT_METHOD_DESTROY] = (void *)widgetScrollPaneDestroy, [WGT_METHOD_SCROLL_CHILD_INTO_VIEW] = (void *)wgtScrollPaneScrollToChild, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetScrollPaneOnDragUpdate, } }; // ============================================================ // Widget creation functions // ============================================================ WidgetT *wgtScrollPane(WidgetT *parent) { WidgetT *w = widgetAlloc(parent, sTypeId); if (w) { ScrollPaneDataT *sp = (ScrollPaneDataT *)calloc(1, sizeof(ScrollPaneDataT)); if (sp) { w->data = sp; } w->weight = 100; } return w; } void wgtScrollPaneSetNoBorder(WidgetT *w, bool noBorder) { VALIDATE_WIDGET_VOID(w, sTypeId); ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; sp->noBorder = noBorder; wgtInvalidate(w); } void wgtScrollPaneScrollToChild(WidgetT *w, const WidgetT *child) { VALIDATE_WIDGET_VOID(w, sTypeId); ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; 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); // Child's virtual offset within the scrollable content int32_t childOffY = child->y - w->y - spBorder(w) + sp->scrollPosV; int32_t maxScrollV = contentMinH - innerH; if (maxScrollV < 0) { maxScrollV = 0; } if (childOffY < sp->scrollPosV) { sp->scrollPosV = childOffY; } else if (childOffY + child->h > sp->scrollPosV + innerH) { sp->scrollPosV = childOffY + child->h - innerH; } sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV); } void wgtScrollPaneScrollToTop(WidgetT *w) { VALIDATE_WIDGET_VOID(w, sTypeId); ScrollPaneDataT *sp = (ScrollPaneDataT *)w->data; sp->scrollPosV = 0; sp->scrollPosH = 0; } // ============================================================ // DXE registration // ============================================================ static const struct { WidgetT *(*create)(WidgetT *parent); void (*scrollToChild)(WidgetT *sp, const WidgetT *child); void (*setNoBorder)(WidgetT *w, bool noBorder); void (*scrollToTop)(WidgetT *w); } sApi = { .create = wgtScrollPane, .scrollToChild = wgtScrollPaneScrollToChild, .setNoBorder = wgtScrollPaneSetNoBorder, .scrollToTop = wgtScrollPaneScrollToTop }; static bool spGetNoBorder(const WidgetT *w) { return ((ScrollPaneDataT *)w->data)->noBorder; } static void spSetNoBorder(WidgetT *w, bool v) { wgtScrollPaneSetNoBorder(w, v); } static const WgtPropDescT sProps[] = { { "NoBorder", WGT_IFACE_BOOL, (void *)spGetNoBorder, (void *)spSetNoBorder, NULL }, }; static const WgtIfaceT sIface = { .basName = "ScrollPane", .props = sProps, .propCount = 1, .methods = NULL, .methodCount = 0, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT, .isContainer = true, .namePrefix = "Scroll", }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassScrollPane); wgtRegisterApi("scrollpane", &sApi); wgtRegisterIface("scrollpane", &sIface); }