Fixed scrollbar thumb dragging and ALT-SPACE.

This commit is contained in:
Scott Duensing 2026-03-18 19:09:29 -05:00
parent 1a60fdcf54
commit 8550fbbbf2
10 changed files with 492 additions and 4 deletions

View file

@ -2936,7 +2936,8 @@ static void pollKeyboard(AppContextT *ctx) {
// Alt+Space — open/close system menu
// Enhanced INT 16h: Alt+Space returns scancode 0x39, ascii 0x20
if (scancode == 0x39 && ascii == 0x20) {
// Must check Alt modifier (bit 3) to distinguish from plain Space
if (scancode == 0x39 && ascii == 0x20 && (shiftFlags & 0x08)) {
if (ctx->sysMenu.active) {
closeSysMenu(ctx);
} else if (ctx->stack.focusedIdx >= 0) {

View file

@ -52,6 +52,9 @@ int32_t sResizeOrigW = 0; // column width at resize start
WidgetT *sDragSplitter = NULL; // splitter being dragged
int32_t sDragSplitStart = 0; // mouse offset from splitter edge at drag start
WidgetT *sDragReorder = NULL; // list/tree widget in drag-reorder mode
WidgetT *sDragScrollbar = NULL; // widget whose scrollbar thumb is being dragged
int32_t sDragScrollbarOff = 0; // mouse offset within thumb at drag start
int32_t sDragScrollbarOrient = 0; // 0=vertical, 1=horizontal
// ============================================================
@ -201,6 +204,10 @@ void widgetDestroyChildren(WidgetT *w) {
sResizeCol = -1;
}
if (sDragScrollbar == child) {
sDragScrollbar = NULL;
}
free(child);
child = next;
}

View file

@ -388,6 +388,20 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
return;
}
// Handle scrollbar thumb drag release
if (sDragScrollbar && !(buttons & MOUSE_LEFT)) {
sDragScrollbar = NULL;
wgtInvalidatePaint(root);
return;
}
// Handle scrollbar thumb drag (mouse move while pressed)
if (sDragScrollbar && (buttons & MOUSE_LEFT)) {
widgetScrollbarDragUpdate(sDragScrollbar, sDragScrollbarOrient, sDragScrollbarOff, x, y);
wgtInvalidatePaint(root);
return;
}
// Handle button press release
if (sPressedButton && !(buttons & MOUSE_LEFT)) {
if (sPressedButton->type == WidgetImageButtonE) {

View file

@ -191,6 +191,9 @@ extern int32_t sResizeOrigW; // original column width at start of resize
extern WidgetT *sDragSplitter; // splitter being dragged
extern int32_t sDragSplitStart; // mouse position at start of splitter drag
extern WidgetT *sDragReorder; // listbox/treeview item being drag-reordered
extern WidgetT *sDragScrollbar; // widget whose scrollbar thumb is being dragged
extern int32_t sDragScrollbarOff; // mouse offset within thumb at drag start
extern int32_t sDragScrollbarOrient; // 0=vertical, 1=horizontal
// ============================================================
// Core functions (widgetCore.c)
@ -260,6 +263,7 @@ typedef enum {
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);
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);
void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos);
// ============================================================

View file

@ -471,6 +471,15 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
} else if (sh == ScrollHitPageIncE) {
hit->as.listBox.scrollPos += visibleRows;
hit->as.listBox.scrollPos = clampInt(hit->as.listBox.scrollPos, 0, maxScroll);
} else if (sh == ScrollHitThumbE) {
int32_t trackLen = innerH - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, hit->as.listBox.itemCount, visibleRows, hit->as.listBox.scrollPos, &thumbPos, &thumbSize);
sDragScrollbar = hit;
sDragScrollbarOrient = 0;
sDragScrollbarOff = relY - WGT_SB_W - thumbPos;
}
hit->focused = true;

View file

@ -817,6 +817,15 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
} else if (sh == ScrollHitPageIncE) {
hit->as.listView->scrollPos += visibleRows;
hit->as.listView->scrollPos = clampInt(hit->as.listView->scrollPos, 0, maxScrollV);
} else if (sh == ScrollHitThumbE) {
int32_t trackLen = innerH - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, hit->as.listView->rowCount, visibleRows, hit->as.listView->scrollPos, &thumbPos, &thumbSize);
sDragScrollbar = hit;
sDragScrollbarOrient = 0;
sDragScrollbarOff = relY - WGT_SB_W - thumbPos;
}
return;
@ -846,6 +855,15 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
hit->as.listView->scrollPosH -= pageSize;
} else if (sh == ScrollHitPageIncE) {
hit->as.listView->scrollPosH += pageSize;
} else if (sh == ScrollHitThumbE) {
int32_t trackLen = innerW - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalColW, innerW, hit->as.listView->scrollPosH, &thumbPos, &thumbSize);
sDragScrollbar = hit;
sDragScrollbarOrient = 1;
sDragScrollbarOff = relX - WGT_SB_W - thumbPos;
}
hit->as.listView->scrollPosH = clampInt(hit->as.listView->scrollPosH, 0, maxScrollH);

View file

@ -499,6 +499,11 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
hit->as.scrollPane.scrollPosV -= pageSize;
} else if (trackRelY >= thumbPos + thumbSize) {
hit->as.scrollPane.scrollPosV += pageSize;
} else {
sDragScrollbar = hit;
sDragScrollbarOrient = 0;
sDragScrollbarOff = trackRelY - thumbPos;
return;
}
}
@ -538,6 +543,11 @@ void widgetScrollPaneOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy
hit->as.scrollPane.scrollPosH -= pageSize;
} else if (trackRelX >= thumbPos + thumbSize) {
hit->as.scrollPane.scrollPosH += pageSize;
} else {
sDragScrollbar = hit;
sDragScrollbarOrient = 1;
sDragScrollbarOff = trackRelX - thumbPos;
return;
}
}

View file

@ -22,6 +22,16 @@
#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
@ -135,6 +145,393 @@ void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
}
// ============================================================
// 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 by measuring children
int32_t contentMinW = 0;
int32_t contentMinH = 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;
}
}
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
// ============================================================

View file

@ -1850,6 +1850,11 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
} else if (trackRelX >= thumbPos + thumbSize) {
w->as.textArea.scrollCol += visCols;
w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll);
} else {
sDragScrollbar = w;
sDragScrollbarOrient = 1;
sDragScrollbarOff = trackRelX - thumbPos;
return;
}
}
@ -1889,6 +1894,11 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
} else if (trackRelY >= thumbPos + thumbSize) {
w->as.textArea.scrollRow += visRows;
w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll);
} else {
sDragScrollbar = w;
sDragScrollbarOrient = 0;
sDragScrollbarOff = trackRelY - thumbPos;
return;
}
}

View file

@ -1056,6 +1056,15 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
hit->as.treeView.scrollPos -= pageSize;
} else if (sh == ScrollHitPageIncE) {
hit->as.treeView.scrollPos += pageSize;
} else if (sh == ScrollHitThumbE) {
int32_t trackLen = innerH - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalH, innerH, hit->as.treeView.scrollPos, &thumbPos, &thumbSize);
sDragScrollbar = hit;
sDragScrollbarOrient = 0;
sDragScrollbarOff = relY - WGT_SB_W - thumbPos;
}
hit->as.treeView.scrollPos = clampInt(hit->as.treeView.scrollPos, 0, maxScrollV);
@ -1085,6 +1094,15 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
hit->as.treeView.scrollPosH -= pageSize;
} else if (sh == ScrollHitPageIncE) {
hit->as.treeView.scrollPosH += pageSize;
} else if (sh == ScrollHitThumbE) {
int32_t trackLen = innerW - WGT_SB_W * 2;
int32_t thumbPos;
int32_t thumbSize;
widgetScrollbarThumb(trackLen, totalW, innerW, hit->as.treeView.scrollPosH, &thumbPos, &thumbSize);
sDragScrollbar = hit;
sDragScrollbarOrient = 1;
sDragScrollbarOff = relX - WGT_SB_W - thumbPos;
}
hit->as.treeView.scrollPosH = clampInt(hit->as.treeView.scrollPosH, 0, maxScrollH);