586 lines
18 KiB
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;
|
|
}
|