708 lines
22 KiB
C
708 lines
22 KiB
C
// widgetCore.c -- Core widget infrastructure (alloc, tree ops, helpers)
|
|
//
|
|
// This file provides the foundation for the widget tree: allocation,
|
|
// parent-child linking, focus management, hit testing, and shared
|
|
// utility functions used across multiple widget types.
|
|
//
|
|
// Widgets form a tree using intrusive linked lists (firstChild/lastChild/
|
|
// nextSibling pointers inside each WidgetT). This is a singly-linked
|
|
// child list with a tail pointer for O(1) append. The tree is owned
|
|
// by its root, which is attached to a WindowT. Destroying the root
|
|
// recursively destroys all descendants.
|
|
//
|
|
// Memory allocation is plain malloc/free rather than an arena or pool.
|
|
// The widget count per window is typically small (tens to low hundreds),
|
|
// so the allocation overhead is negligible on target hardware. An arena
|
|
// approach was considered but rejected because widgets can be individually
|
|
// created and destroyed at runtime (dialog dynamics, tree item insertion),
|
|
// which doesn't map cleanly to an arena pattern.
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
// ============================================================
|
|
// Global state for drag and popup tracking
|
|
// ============================================================
|
|
//
|
|
// These module-level pointers track ongoing UI interactions that span
|
|
// multiple mouse events (drags, popups, button presses). They are global
|
|
// rather than per-window because the DOS GUI is single-threaded and only
|
|
// one interaction can be active at a time.
|
|
//
|
|
// Each pointer is set when an interaction begins (e.g. mouse-down on a
|
|
// slider) and cleared when it ends (mouse-up). The event dispatcher in
|
|
// widgetEvent.c checks these before normal hit testing -- active drags
|
|
// take priority over everything else.
|
|
//
|
|
// All of these must be NULLed when the pointed-to widget is destroyed,
|
|
// otherwise dangling pointers would cause crashes. widgetDestroyChildren()
|
|
// and wgtDestroy() handle this cleanup.
|
|
|
|
clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint
|
|
bool sDebugLayout = false;
|
|
WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk)
|
|
WidgetT *sOpenPopup = NULL; // dropdown/combobox with open popup list
|
|
WidgetT *sPressedButton = NULL; // button being held down (tracks mouse in/out)
|
|
WidgetT *sDragSlider = NULL; // slider being dragged
|
|
WidgetT *sDrawingCanvas = NULL; // canvas receiving paint strokes
|
|
WidgetT *sDragTextSelect = NULL; // text widget in drag-select mode
|
|
int32_t sDragOffset = 0; // pixel offset from drag start to thumb center
|
|
WidgetT *sResizeListView = NULL; // ListView undergoing column resize
|
|
int32_t sResizeCol = -1; // which column is being resized
|
|
int32_t sResizeStartX = 0; // mouse X at resize start
|
|
int32_t sResizeOrigW = 0; // column width at resize start
|
|
bool sResizeDragging = false; // true once mouse moves during column resize
|
|
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
|
|
|
|
|
|
// ============================================================
|
|
// widgetAddChild
|
|
// ============================================================
|
|
//
|
|
// Appends a child to the end of the parent's child list. O(1)
|
|
// thanks to the lastChild tail pointer. The child list is singly-
|
|
// linked (nextSibling), which saves 4 bytes per widget vs doubly-
|
|
// linked and is sufficient because child removal is infrequent.
|
|
|
|
void widgetAddChild(WidgetT *parent, WidgetT *child) {
|
|
child->parent = parent;
|
|
child->nextSibling = NULL;
|
|
|
|
if (parent->lastChild) {
|
|
parent->lastChild->nextSibling = child;
|
|
parent->lastChild = child;
|
|
} else {
|
|
parent->firstChild = child;
|
|
parent->lastChild = child;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetAlloc
|
|
// ============================================================
|
|
//
|
|
// Allocates and zero-initializes a new widget, links it to its
|
|
// class vtable via widgetClassTable[], and optionally adds it as
|
|
// a child of the given parent.
|
|
//
|
|
// The memset to 0 is intentional -- it establishes sane defaults
|
|
// for all fields: NULL pointers, zero coordinates, no focus,
|
|
// no accel key, etc. Only visible and enabled default to true.
|
|
//
|
|
// The window pointer is inherited from the parent so that any
|
|
// widget in the tree can find its owning window without walking
|
|
// to the root.
|
|
|
|
WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) {
|
|
WidgetT *w = (WidgetT *)malloc(sizeof(WidgetT));
|
|
|
|
if (!w) {
|
|
return NULL;
|
|
}
|
|
|
|
memset(w, 0, sizeof(*w));
|
|
w->type = type;
|
|
w->wclass = widgetClassTable[type];
|
|
w->visible = true;
|
|
w->enabled = true;
|
|
|
|
if (parent) {
|
|
w->window = parent->window;
|
|
widgetAddChild(parent, w);
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetClearFocus
|
|
// ============================================================
|
|
|
|
void widgetClearFocus(WidgetT *root) {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
root->focused = false;
|
|
|
|
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
|
|
widgetClearFocus(c);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetCountVisibleChildren
|
|
// ============================================================
|
|
|
|
int32_t widgetCountVisibleChildren(const WidgetT *w) {
|
|
int32_t count = 0;
|
|
|
|
for (const WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->visible) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDestroyChildren
|
|
// ============================================================
|
|
//
|
|
// Recursively destroys all descendants of a widget. Processes
|
|
// children depth-first (destroy grandchildren before the child
|
|
// itself) so that per-widget destroy callbacks see a consistent
|
|
// tree state.
|
|
//
|
|
// Critically, this function clears all global state pointers that
|
|
// reference destroyed widgets. Without this, any pending drag or
|
|
// focus state would become a dangling pointer. Each global is
|
|
// checked individually rather than cleared unconditionally to
|
|
// avoid disrupting unrelated ongoing interactions.
|
|
|
|
void widgetDestroyChildren(WidgetT *w) {
|
|
WidgetT *child = w->firstChild;
|
|
|
|
while (child) {
|
|
WidgetT *next = child->nextSibling;
|
|
widgetDestroyChildren(child);
|
|
|
|
if (child->wclass && child->wclass->destroy) {
|
|
child->wclass->destroy(child);
|
|
}
|
|
|
|
// Clear static references if they point to destroyed widgets
|
|
if (sFocusedWidget == child) {
|
|
sFocusedWidget = NULL;
|
|
}
|
|
|
|
if (sOpenPopup == child) {
|
|
sOpenPopup = NULL;
|
|
}
|
|
|
|
if (sPressedButton == child) {
|
|
sPressedButton = NULL;
|
|
}
|
|
|
|
if (sDragSlider == child) {
|
|
sDragSlider = NULL;
|
|
}
|
|
|
|
if (sDrawingCanvas == child) {
|
|
sDrawingCanvas = NULL;
|
|
}
|
|
|
|
if (sResizeListView == child) {
|
|
sResizeListView = NULL;
|
|
sResizeCol = -1;
|
|
sResizeDragging = false;
|
|
}
|
|
|
|
if (sDragScrollbar == child) {
|
|
sDragScrollbar = NULL;
|
|
}
|
|
|
|
free(child);
|
|
child = next;
|
|
}
|
|
|
|
w->firstChild = NULL;
|
|
w->lastChild = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetDropdownPopupRect
|
|
// ============================================================
|
|
//
|
|
// Calculates the screen rectangle for a dropdown/combobox popup list.
|
|
// Shared between Dropdown and ComboBox since they have identical
|
|
// popup positioning logic.
|
|
//
|
|
// The popup tries to open below the widget first. If there isn't
|
|
// enough room (popup would extend past the content area bottom),
|
|
// it flips to open above instead. This ensures the popup is always
|
|
// visible, even for dropdowns near the bottom of a window.
|
|
//
|
|
// Popup height is capped at DROPDOWN_MAX_VISIBLE items to prevent
|
|
// huge popups from dominating the screen.
|
|
|
|
void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) {
|
|
int32_t itemCount = 0;
|
|
|
|
if (w->type == WidgetDropdownE) {
|
|
itemCount = w->as.dropdown.itemCount;
|
|
} else if (w->type == WidgetComboBoxE) {
|
|
itemCount = w->as.comboBox.itemCount;
|
|
}
|
|
|
|
int32_t visibleItems = itemCount;
|
|
|
|
if (visibleItems > DROPDOWN_MAX_VISIBLE) {
|
|
visibleItems = DROPDOWN_MAX_VISIBLE;
|
|
}
|
|
|
|
if (visibleItems < 1) {
|
|
visibleItems = 1;
|
|
}
|
|
|
|
*popX = w->x;
|
|
*popW = w->w;
|
|
*popH = visibleItems * font->charHeight + 4; // 2px border each side
|
|
|
|
// Try below first, then above if no room
|
|
if (w->y + w->h + *popH <= contentH) {
|
|
*popY = w->y + w->h;
|
|
} else {
|
|
*popY = w->y - *popH;
|
|
|
|
if (*popY < 0) {
|
|
*popY = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetFindByAccel
|
|
// ============================================================
|
|
//
|
|
// Finds a widget with the given Alt+key accelerator. Recurses the
|
|
// tree depth-first, respecting visibility and enabled state.
|
|
//
|
|
// Special case for TabPage widgets: even if the tab page itself is
|
|
// not visible (inactive tab), its accelKey is still checked. This
|
|
// allows Alt+key to switch to a different tab. However, children
|
|
// of invisible tab pages are NOT searched -- their accelerators
|
|
// should not be active when the tab is hidden.
|
|
|
|
WidgetT *widgetFindByAccel(WidgetT *root, char key) {
|
|
if (!root || !root->enabled) {
|
|
return NULL;
|
|
}
|
|
|
|
// Invisible tab pages: match the page itself (for tab switching)
|
|
// but don't recurse into children (their accels shouldn't be active)
|
|
if (!root->visible) {
|
|
if (root->type == WidgetTabPageE && root->accelKey == key) {
|
|
return root;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
if (root->accelKey == key) {
|
|
return root;
|
|
}
|
|
|
|
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
|
|
WidgetT *found = widgetFindByAccel(c, key);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetFindNextFocusable
|
|
// ============================================================
|
|
//
|
|
// Implements Tab-order navigation: finds the next focusable widget
|
|
// after 'after' in depth-first tree order. The two-pass approach
|
|
// (search from 'after' to end, then wrap to start) ensures circular
|
|
// tabbing -- Tab on the last focusable widget wraps to the first.
|
|
//
|
|
// The pastAfter flag tracks whether we've passed the 'after' widget
|
|
// during traversal. Once past it, the next focusable widget is the
|
|
// answer. This avoids collecting all focusable widgets into an array
|
|
// just to find the next one -- the common case returns quickly.
|
|
|
|
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter) {
|
|
if (!w->visible || !w->enabled) {
|
|
return NULL;
|
|
}
|
|
|
|
if (after == NULL) {
|
|
*pastAfter = true;
|
|
}
|
|
|
|
if (w == after) {
|
|
*pastAfter = true;
|
|
} else if (*pastAfter && widgetIsFocusable(w->type)) {
|
|
return w;
|
|
}
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
WidgetT *found = findNextFocusableImpl(c, after, pastAfter);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after) {
|
|
bool pastAfter = false;
|
|
WidgetT *found = findNextFocusableImpl(root, after, &pastAfter);
|
|
|
|
if (found) {
|
|
return found;
|
|
}
|
|
|
|
// Wrap around -- search from the beginning
|
|
pastAfter = true;
|
|
return findNextFocusableImpl(root, NULL, &pastAfter);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetFindPrevFocusable
|
|
// ============================================================
|
|
//
|
|
// Shift+Tab navigation: finds the previous focusable widget.
|
|
// Unlike findNextFocusable which can short-circuit during traversal,
|
|
// finding the PREVIOUS widget requires knowing the full order.
|
|
// So this collects all focusable widgets into an array, finds the
|
|
// target's index, and returns index-1 (with wraparound).
|
|
//
|
|
// The explicit stack-based DFS (rather than recursion) is used here
|
|
// because we need to push children in reverse order to get the same
|
|
// left-to-right depth-first ordering as the recursive version.
|
|
// Fixed-size arrays (128 widgets, 64 stack depth) are adequate for
|
|
// any reasonable dialog layout and avoid dynamic allocation.
|
|
|
|
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
|
|
WidgetT *list[128];
|
|
int32_t count = 0;
|
|
|
|
// Collect all focusable widgets via depth-first traversal
|
|
WidgetT *stack[64];
|
|
int32_t top = 0;
|
|
stack[top++] = root;
|
|
|
|
while (top > 0) {
|
|
WidgetT *w = stack[--top];
|
|
|
|
if (!w->visible || !w->enabled) {
|
|
continue;
|
|
}
|
|
|
|
if (widgetIsFocusable(w->type) && count < 128) {
|
|
list[count++] = w;
|
|
}
|
|
|
|
// Push children in reverse order so first child is processed first
|
|
WidgetT *children[64];
|
|
int32_t childCount = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (childCount < 64) {
|
|
children[childCount++] = c;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = childCount - 1; i >= 0; i--) {
|
|
if (top < 64) {
|
|
stack[top++] = children[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (count == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
// Find 'before' in the list
|
|
int32_t idx = -1;
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
if (list[i] == before) {
|
|
idx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (idx <= 0) {
|
|
return list[count - 1]; // Wrap to last
|
|
}
|
|
|
|
return list[idx - 1];
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetFrameBorderWidth
|
|
// ============================================================
|
|
|
|
int32_t widgetFrameBorderWidth(const WidgetT *w) {
|
|
if (w->type != WidgetFrameE) {
|
|
return 0;
|
|
}
|
|
|
|
if (w->as.frame.style == FrameFlatE) {
|
|
return FRAME_FLAT_BORDER;
|
|
}
|
|
|
|
return FRAME_BEVEL_BORDER;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetHitTest
|
|
// ============================================================
|
|
//
|
|
// Recursive hit testing: finds the deepest (most specific) widget
|
|
// under the given coordinates. Returns the widget itself if no
|
|
// child is hit, or NULL if the point is outside this widget.
|
|
//
|
|
// Children are iterated front-to-back (first to last in the linked
|
|
// list), but the LAST match wins. This gives later siblings higher
|
|
// Z-order, which matches the painting order (later children paint
|
|
// on top of earlier ones). This is important for overlapping widgets,
|
|
// though in practice the layout engine rarely produces overlap.
|
|
//
|
|
// Widgets with WCLASS_NO_HIT_RECURSE stop the recursion -- the parent
|
|
// widget handles all mouse events for its children. This is used by
|
|
// TreeView, ScrollPane, ListView, and Splitter, which need to manage
|
|
// their own internal regions (scrollbars, column headers, tree
|
|
// expand buttons) that don't correspond to child widgets.
|
|
|
|
WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) {
|
|
if (!w->visible) {
|
|
return NULL;
|
|
}
|
|
|
|
if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) {
|
|
return NULL;
|
|
}
|
|
|
|
// Widgets with WCLASS_NO_HIT_RECURSE manage their own children
|
|
if (w->wclass && (w->wclass->flags & WCLASS_NO_HIT_RECURSE)) {
|
|
return w;
|
|
}
|
|
|
|
// Check children -- take the last match (topmost in Z-order)
|
|
WidgetT *hit = NULL;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
WidgetT *childHit = widgetHitTest(c, x, y);
|
|
|
|
if (childHit) {
|
|
hit = childHit;
|
|
}
|
|
}
|
|
|
|
return hit ? hit : w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetIsFocusable
|
|
// ============================================================
|
|
|
|
bool widgetIsFocusable(WidgetTypeE type) {
|
|
return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetIsBoxContainer
|
|
// ============================================================
|
|
//
|
|
// Returns true for widget types that use the generic box layout.
|
|
|
|
bool widgetIsBoxContainer(WidgetTypeE type) {
|
|
return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetIsHorizContainer
|
|
// ============================================================
|
|
//
|
|
// Returns true for container types that lay out children horizontally.
|
|
|
|
bool widgetIsHorizContainer(WidgetTypeE type) {
|
|
return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetNavigateIndex
|
|
// ============================================================
|
|
//
|
|
// Shared keyboard navigation for list-like widgets (ListBox, Dropdown,
|
|
// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic
|
|
// so each widget doesn't have to reimplement index clamping.
|
|
//
|
|
// Key values use the 0x100 flag to mark extended scan codes (arrow
|
|
// keys, Home, End, etc.) -- this is the DVX convention for passing
|
|
// scan codes through the same int32_t channel as ASCII values.
|
|
//
|
|
// Returns -1 for unrecognized keys so callers can check whether the
|
|
// key was consumed.
|
|
|
|
int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) {
|
|
if (key == (0x50 | 0x100)) {
|
|
// Down arrow
|
|
if (current < count - 1) {
|
|
return current + 1;
|
|
}
|
|
|
|
return current < 0 ? 0 : current;
|
|
}
|
|
|
|
if (key == (0x48 | 0x100)) {
|
|
// Up arrow
|
|
if (current > 0) {
|
|
return current - 1;
|
|
}
|
|
|
|
return current < 0 ? 0 : current;
|
|
}
|
|
|
|
if (key == (0x47 | 0x100)) {
|
|
// Home
|
|
return 0;
|
|
}
|
|
|
|
if (key == (0x4F | 0x100)) {
|
|
// End
|
|
return count - 1;
|
|
}
|
|
|
|
if (key == (0x51 | 0x100)) {
|
|
// Page Down
|
|
int32_t n = current + pageSize;
|
|
return n >= count ? count - 1 : n;
|
|
}
|
|
|
|
if (key == (0x49 | 0x100)) {
|
|
// Page Up
|
|
int32_t n = current - pageSize;
|
|
return n < 0 ? 0 : n;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetPaintPopupList
|
|
// ============================================================
|
|
//
|
|
// Shared popup list painting for Dropdown and ComboBox.
|
|
|
|
void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) {
|
|
// Draw popup border
|
|
BevelStyleT bevel;
|
|
bevel.highlight = colors->windowHighlight;
|
|
bevel.shadow = colors->windowShadow;
|
|
bevel.face = colors->contentBg;
|
|
bevel.width = 2;
|
|
drawBevel(d, ops, popX, popY, popW, popH, &bevel);
|
|
|
|
// Draw items
|
|
int32_t visibleItems = popH / font->charHeight;
|
|
int32_t textX = popX + TEXT_INPUT_PAD;
|
|
int32_t textY = popY + 2;
|
|
int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4;
|
|
|
|
for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) {
|
|
int32_t idx = scrollPos + i;
|
|
int32_t iy = textY + i * font->charHeight;
|
|
uint32_t ifg = colors->contentFg;
|
|
uint32_t ibg = colors->contentBg;
|
|
|
|
if (idx == hoverIdx) {
|
|
ifg = colors->menuHighlightFg;
|
|
ibg = colors->menuHighlightBg;
|
|
rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg);
|
|
}
|
|
|
|
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetScrollbarThumb
|
|
// ============================================================
|
|
//
|
|
// Calculates thumb position and size for a scrollbar track.
|
|
// Used by both the WM-level scrollbars and widget-internal scrollbars
|
|
// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior.
|
|
//
|
|
// The thumb size is proportional to visibleSize/totalSize -- a larger
|
|
// visible area means a larger thumb, giving visual feedback about how
|
|
// much content is scrollable. SB_MIN_THUMB prevents the thumb from
|
|
// becoming too small to grab with a mouse.
|
|
|
|
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
|
|
*thumbSize = (trackLen * visibleSize) / totalSize;
|
|
|
|
if (*thumbSize < SB_MIN_THUMB) {
|
|
*thumbSize = SB_MIN_THUMB;
|
|
}
|
|
|
|
if (*thumbSize > trackLen) {
|
|
*thumbSize = trackLen;
|
|
}
|
|
|
|
int32_t maxScroll = totalSize - visibleSize;
|
|
|
|
if (maxScroll > 0) {
|
|
*thumbPos = ((trackLen - *thumbSize) * scrollPos) / maxScroll;
|
|
} else {
|
|
*thumbPos = 0;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetRemoveChild
|
|
// ============================================================
|
|
//
|
|
// Unlinks a child from its parent's child list. O(n) in the number
|
|
// of children because the singly-linked list requires walking to
|
|
// find the predecessor. This is acceptable because child removal
|
|
// is infrequent (widget destruction, tree item reordering).
|
|
|
|
void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
|
|
WidgetT *prev = NULL;
|
|
|
|
for (WidgetT *c = parent->firstChild; c; c = c->nextSibling) {
|
|
if (c == child) {
|
|
if (prev) {
|
|
prev->nextSibling = c->nextSibling;
|
|
} else {
|
|
parent->firstChild = c->nextSibling;
|
|
}
|
|
|
|
if (parent->lastChild == child) {
|
|
parent->lastChild = prev;
|
|
}
|
|
|
|
child->nextSibling = NULL;
|
|
child->parent = NULL;
|
|
return;
|
|
}
|
|
|
|
prev = c;
|
|
}
|
|
}
|