DVX_GUI/dvx/widgets/widgetScrollPane.c

617 lines
18 KiB
C

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