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