936 lines
30 KiB
C
936 lines
30 KiB
C
#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;
|
|
} ScrollPaneDataT;
|
|
|
|
#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);
|
|
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 - 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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 + SP_BORDER - sp->scrollPosH;
|
|
int32_t baseY = w->y + SP_BORDER - 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 + SP_BORDER - sp->scrollPosH;
|
|
baseY = w->y + SP_BORDER - 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) {
|
|
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 - 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) {
|
|
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 - 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) {
|
|
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 - 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 && 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 + SP_BORDER;
|
|
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 + SP_BORDER;
|
|
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, 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 == 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 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 - SP_BORDER + 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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent);
|
|
void (*scrollToChild)(WidgetT *sp, const WidgetT *child);
|
|
} sApi = {
|
|
.create = wgtScrollPane,
|
|
.scrollToChild = wgtScrollPaneScrollToChild
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "ScrollPane",
|
|
.props = NULL,
|
|
.propCount = 0,
|
|
.methods = NULL,
|
|
.methodCount = 0,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.isContainer = true
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassScrollPane);
|
|
wgtRegisterApi("scrollpane", &sApi);
|
|
wgtRegisterIface("scrollpane", &sIface);
|
|
}
|