DVX_GUI/dvx/widgets/widgetScrollbar.c

586 lines
18 KiB
C

// widgetScrollbar.c -- Shared scrollbar painting and hit-testing
//
// These are not widgets themselves -- they are stateless rendering and
// hit-testing utilities shared by ScrollPane, TreeView, TextArea,
// ListBox, and ListView. Each owning widget stores its own scroll
// position; these functions are purely geometric.
//
// The scrollbar model uses three parts: two arrow buttons (one at each
// end) and a proportional thumb in the track between them. Thumb size
// is proportional to (visibleSize / totalSize), clamped to SB_MIN_THUMB
// to remain grabbable even when content is very large. Thumb position
// maps linearly from scrollPos to track position.
//
// Arrow triangles are drawn with simple loop-based scanlines (4 rows),
// producing 7-pixel-wide arrow glyphs. This avoids any font or bitmap
// dependency for the scrollbar chrome.
//
// The minimum scrollbar length guard (sbW < WGT_SB_W * 3) ensures
// there is at least room for both arrow buttons plus a minimal track.
// If the container is too small, the scrollbar is simply not drawn
// rather than rendering a corrupted mess.
#include "widgetInternal.h"
// Constants duplicated from widgetTextInput.c and widgetScrollPane.c
// for use in widgetScrollbarDragUpdate. These are file-local in their
// source files, so we repeat the values here to avoid cross-file
// coupling. They must stay in sync with the originals.
#define TEXTAREA_BORDER 2
#define TEXTAREA_PAD 2
#define TEXTAREA_SB_W 14
#define SP_BORDER 2
#define SP_SB_W 14
// ============================================================
// widgetDrawScrollbarH
// ============================================================
void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
if (sbW < WGT_SB_W * 3) {
return;
}
// Trough background
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
drawBevel(d, ops, sbX, sbY, sbW, WGT_SB_W, &troughBevel);
// Left arrow button
BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
// Left arrow triangle
{
int32_t cx = sbX + WGT_SB_W / 2;
int32_t cy = sbY + WGT_SB_W / 2;
uint32_t fg = colors->scrollbarFg;
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 - WGT_SB_W;
drawBevel(d, ops, rightX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
// Right arrow triangle
{
int32_t cx = rightX + WGT_SB_W / 2;
int32_t cy = sbY + WGT_SB_W / 2;
uint32_t fg = colors->scrollbarFg;
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 - WGT_SB_W * 2;
if (trackLen > 0 && totalSize > 0) {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawBevel(d, ops, sbX + WGT_SB_W + thumbPos, sbY, thumbSize, WGT_SB_W, &btnBevel);
}
}
// ============================================================
// widgetDrawScrollbarV
// ============================================================
void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
if (sbH < WGT_SB_W * 3) {
return;
}
// Trough background
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
drawBevel(d, ops, sbX, sbY, WGT_SB_W, sbH, &troughBevel);
// Up arrow button
BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
drawBevel(d, ops, sbX, sbY, WGT_SB_W, WGT_SB_W, &btnBevel);
// Up arrow triangle
{
int32_t cx = sbX + WGT_SB_W / 2;
int32_t cy = sbY + WGT_SB_W / 2;
uint32_t fg = colors->scrollbarFg;
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 - WGT_SB_W;
drawBevel(d, ops, sbX, downY, WGT_SB_W, WGT_SB_W, &btnBevel);
// Down arrow triangle
{
int32_t cx = sbX + WGT_SB_W / 2;
int32_t cy = downY + WGT_SB_W / 2;
uint32_t fg = colors->scrollbarFg;
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 - WGT_SB_W * 2;
if (trackLen > 0 && totalSize > 0) {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
drawBevel(d, ops, sbX, sbY + WGT_SB_W + thumbPos, WGT_SB_W, thumbSize, &btnBevel);
}
}
// ============================================================
// widgetScrollbarDragUpdate
// ============================================================
// Handles ongoing scrollbar thumb drag for widget-internal scrollbars.
// Converts the mouse pixel position into a scroll value using linear
// interpolation, matching the WM-level wmScrollbarDrag logic.
// orient: 0=vertical, 1=horizontal.
// dragOff: mouse offset within thumb captured at drag start.
//
// Each widget type stores scroll state differently (row counts vs
// pixel offsets, different struct fields), so this function switches
// on widget type to extract the scrollbar geometry and update the
// correct scroll field.
void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
// Determine scrollbar geometry per widget type.
// sbOrigin: screen coordinate of the scrollbar's top/left edge
// sbLen: total length of the scrollbar (including arrow buttons)
// totalSize: total content size (items, pixels, or columns)
// visibleSize: visible portion of content
// scrollPos: current scroll position (pointer to update)
// maxScroll: maximum scroll value
int32_t sbOrigin = 0;
int32_t sbLen = 0;
int32_t totalSize = 0;
int32_t visibleSize = 0;
int32_t maxScroll = 0;
int32_t sbWidth = WGT_SB_W;
if (w->type == WidgetTextAreaE) {
sbWidth = TEXTAREA_SB_W;
if (orient == 0) {
// Vertical scrollbar
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = 0;
// Compute max line length by scanning lines
const char *buf = w->as.textArea.buf;
int32_t len = w->as.textArea.len;
int32_t lineStart = 0;
for (int32_t i = 0; i <= len; i++) {
if (i == len || buf[i] == '\n') {
int32_t ll = i - lineStart;
if (ll > maxLL) {
maxLL = ll;
}
lineStart = i + 1;
}
}
bool needHSb = (maxLL > visCols);
int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0);
int32_t visRows = innerH / font->charHeight;
if (visRows < 1) {
visRows = 1;
}
// Count total lines
int32_t totalLines = 1;
for (int32_t i = 0; i < len; i++) {
if (buf[i] == '\n') {
totalLines++;
}
}
sbOrigin = w->y + TEXTAREA_BORDER;
sbLen = innerH;
totalSize = totalLines;
visibleSize = visRows;
maxScroll = totalLines - visRows;
} else {
// Horizontal scrollbar
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W;
int32_t visCols = innerW / font->charWidth;
int32_t maxLL = 0;
const char *buf = w->as.textArea.buf;
int32_t len = w->as.textArea.len;
int32_t lineStart = 0;
for (int32_t i = 0; i <= len; i++) {
if (i == len || buf[i] == '\n') {
int32_t ll = i - lineStart;
if (ll > maxLL) {
maxLL = ll;
}
lineStart = i + 1;
}
}
int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W;
sbOrigin = w->x + TEXTAREA_BORDER;
sbLen = hsbW;
totalSize = maxLL;
visibleSize = visCols;
maxScroll = maxLL - visCols;
}
} else if (w->type == WidgetListBoxE) {
// Vertical only
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t innerH = w->h - LISTBOX_BORDER * 2;
int32_t visibleRows = innerH / font->charHeight;
sbOrigin = w->y + LISTBOX_BORDER;
sbLen = innerH;
totalSize = w->as.listBox.itemCount;
visibleSize = visibleRows;
maxScroll = totalSize - visibleSize;
} else if (w->type == WidgetListViewE) {
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t headerH = font->charHeight + 4;
int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH;
int32_t innerW = w->w - LISTVIEW_BORDER * 2;
int32_t visibleRows = innerH / font->charHeight;
int32_t totalColW = w->as.listView->totalColW;
bool needVSb = (w->as.listView->rowCount > visibleRows);
if (needVSb) {
innerW -= WGT_SB_W;
}
if (totalColW > innerW) {
innerH -= WGT_SB_W;
visibleRows = innerH / font->charHeight;
if (!needVSb && w->as.listView->rowCount > visibleRows) {
innerW -= WGT_SB_W;
}
}
if (visibleRows < 1) {
visibleRows = 1;
}
if (orient == 0) {
// Vertical
sbOrigin = w->y + LISTVIEW_BORDER + headerH;
sbLen = innerH;
totalSize = w->as.listView->rowCount;
visibleSize = visibleRows;
maxScroll = totalSize - visibleSize;
} else {
// Horizontal
sbOrigin = w->x + LISTVIEW_BORDER;
sbLen = innerW;
totalSize = totalColW;
visibleSize = innerW;
maxScroll = totalColW - innerW;
}
} else if (w->type == WidgetTreeViewE) {
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t totalH;
int32_t totalW;
int32_t innerH;
int32_t innerW;
bool needVSb;
// Walk the visible tree to compute total content dimensions.
// This duplicates treeCalcScrollbarNeeds which is file-static,
// so we compute it inline using the same logic.
int32_t treeH = 0;
int32_t treeW = 0;
// Count visible items and max width
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->type == WidgetTreeItemE && c->visible) {
// Walk visible items
WidgetT *item = c;
while (item) {
treeH += font->charHeight;
// Compute depth
int32_t depth = 0;
WidgetT *p = item->parent;
while (p && p != w) {
depth++;
p = p->parent;
}
int32_t itemW = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP;
if (item->as.treeItem.text) {
itemW += (int32_t)strlen(item->as.treeItem.text) * font->charWidth;
}
if (itemW > treeW) {
treeW = itemW;
}
item = widgetTreeViewNextVisible(item, w);
}
break; // Only process first top-level visible chain
}
}
totalH = treeH;
totalW = treeW;
innerH = w->h - TREE_BORDER * 2;
innerW = w->w - TREE_BORDER * 2;
needVSb = (totalH > innerH);
if (needVSb) {
innerW -= WGT_SB_W;
}
if (totalW > innerW) {
innerH -= WGT_SB_W;
if (!needVSb && totalH > innerH) {
needVSb = true;
innerW -= WGT_SB_W;
}
}
if (orient == 0) {
// Vertical (pixel-based scroll)
sbOrigin = w->y + TREE_BORDER;
sbLen = innerH;
totalSize = totalH;
visibleSize = innerH;
maxScroll = totalH - innerH;
} else {
// Horizontal (pixel-based scroll)
sbOrigin = w->x + TREE_BORDER;
sbLen = innerW;
totalSize = totalW;
visibleSize = innerW;
maxScroll = totalW - innerW;
}
} else if (w->type == WidgetScrollPaneE) {
sbWidth = SP_SB_W;
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
// Compute content min size (must match spCalcNeeds in widgetScrollPane.c)
int32_t contentMinW = 0;
int32_t contentMinH = 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) {
if (c->wclass && c->wclass->calcMinSize) {
c->wclass->calcMinSize(c, font);
}
if (c->calcMinW > contentMinW) {
contentMinW = c->calcMinW;
}
contentMinH += c->calcMinH;
count++;
}
}
if (count > 1) {
contentMinH += gap * (count - 1);
}
contentMinW += pad * 2;
contentMinH += pad * 2;
int32_t innerH = w->h - SP_BORDER * 2;
int32_t innerW = w->w - SP_BORDER * 2;
bool needVSb = (contentMinH > innerH);
if (needVSb) {
innerW -= SP_SB_W;
}
if (contentMinW > innerW) {
innerH -= SP_SB_W;
if (!needVSb && contentMinH > innerH) {
needVSb = true;
innerW -= SP_SB_W;
}
}
if (orient == 0) {
// Vertical
sbOrigin = w->y + SP_BORDER;
sbLen = innerH;
totalSize = contentMinH;
visibleSize = innerH;
maxScroll = contentMinH - innerH;
} else {
// Horizontal
sbOrigin = w->x + SP_BORDER;
sbLen = innerW;
totalSize = contentMinW;
visibleSize = innerW;
maxScroll = contentMinW - innerW;
}
} else {
return;
}
if (maxScroll < 0) {
maxScroll = 0;
}
if (maxScroll == 0) {
return;
}
// Compute thumb geometry
int32_t trackLen = sbLen - sbWidth * 2;
if (trackLen <= 0) {
return;
}
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, 0, &thumbPos, &thumbSize);
if (trackLen <= thumbSize) {
return;
}
// Convert mouse position to scroll value
int32_t mousePos;
if (orient == 0) {
mousePos = mouseY - sbOrigin - sbWidth - dragOff;
} else {
mousePos = mouseX - sbOrigin - sbWidth - dragOff;
}
int32_t newScroll = (mousePos * maxScroll) / (trackLen - thumbSize);
if (newScroll < 0) {
newScroll = 0;
}
if (newScroll > maxScroll) {
newScroll = maxScroll;
}
// Update the widget's scroll position
if (w->type == WidgetTextAreaE) {
if (orient == 0) {
w->as.textArea.scrollRow = newScroll;
} else {
w->as.textArea.scrollCol = newScroll;
}
} else if (w->type == WidgetListBoxE) {
w->as.listBox.scrollPos = newScroll;
} else if (w->type == WidgetListViewE) {
if (orient == 0) {
w->as.listView->scrollPos = newScroll;
} else {
w->as.listView->scrollPosH = newScroll;
}
} else if (w->type == WidgetTreeViewE) {
if (orient == 0) {
w->as.treeView.scrollPos = newScroll;
} else {
w->as.treeView.scrollPosH = newScroll;
}
} else if (w->type == WidgetScrollPaneE) {
if (orient == 0) {
w->as.scrollPane.scrollPosV = newScroll;
} else {
w->as.scrollPane.scrollPosH = newScroll;
}
}
}
// ============================================================
// widgetScrollbarHitTest
// ============================================================
// Axis-agnostic hit test. The caller converts (vx,vy) into a 1D
// position along the scrollbar axis (relPos) and the scrollbar
// length (sbLen). Returns which zone was hit: arrow buttons,
// page-up/page-down trough regions, or the thumb itself.
// This factoring lets all scrollbar-owning widgets share the same
// logic without duplicating per-axis code.
ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos) {
if (relPos < WGT_SB_W) {
return ScrollHitArrowDecE;
}
if (relPos >= sbLen - WGT_SB_W) {
return ScrollHitArrowIncE;
}
int32_t trackLen = sbLen - WGT_SB_W * 2;
if (trackLen > 0) {
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalSize, visibleSize, scrollPos, &thumbPos, &thumbSize);
int32_t trackRel = relPos - WGT_SB_W;
if (trackRel < thumbPos) {
return ScrollHitPageDecE;
}
if (trackRel >= thumbPos + thumbSize) {
return ScrollHitPageIncE;
}
return ScrollHitThumbE;
}
return ScrollHitNoneE;
}