#define DVX_WIDGET_IMPL // 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 "dvxWidgetPlugin.h" #include "stb_ds.h" #include // ============================================================ // 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. bool sCursorBlinkOn = true; // text cursor blink phase (toggled by wgtUpdateCursorBlink) 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 *sDragWidget = NULL; // widget being dragged (any drag type) // Shared clipboard -- process-wide, not per-widget. #define CLIPBOARD_MAX 4096 static char sClipboard[CLIPBOARD_MAX]; static int32_t sClipboardLen = 0; // Multi-click state (used by widgetEvent.c for universal dbl-click detection) static clock_t sLastClickTime = 0; static int32_t sLastClickX = -1; static int32_t sLastClickY = -1; static int32_t sClickCount = 0; // ============================================================ // clipboardCopy // ============================================================ void clipboardCopy(const char *text, int32_t len) { if (!text || len <= 0) { return; } if (len > CLIPBOARD_MAX - 1) { len = CLIPBOARD_MAX - 1; } memcpy(sClipboard, text, len); sClipboard[len] = '\0'; sClipboardLen = len; } // ============================================================ // clipboardGet // ============================================================ const char *clipboardGet(int32_t *outLen) { if (outLen) { *outLen = sClipboardLen; } return sClipboard; } // ============================================================ // clipboardMaxLen // ============================================================ int32_t clipboardMaxLen(void) { return CLIPBOARD_MAX - 1; } // ============================================================ // multiClickDetect // ============================================================ int32_t multiClickDetect(int32_t vx, int32_t vy) { clock_t now = clock(); // Guard against multiple calls in the same frame (e.g. widget onMouse // calls it, then widgetEvent.c calls it again). If coords and time // are identical to the last call, return the cached count. if (now == sLastClickTime && vx == sLastClickX && vy == sLastClickY) { return sClickCount; } if ((now - sLastClickTime) < sDblClickTicks && abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { sClickCount++; } else { sClickCount = 1; } sLastClickTime = now; sLastClickX = vx; sLastClickY = vy; return sClickCount; } // ============================================================ // 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, int32_t type) { if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return NULL; } 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); wclsDestroy(child); // Clear static references if they point to destroyed widgets if (sFocusedWidget == child) { sFocusedWidget = NULL; } if (sOpenPopup == child) { sOpenPopup = NULL; } if (sDragWidget == child) { sDragWidget = NULL; } free(child); child = next; } w->firstChild = NULL; w->lastChild = NULL; } // ============================================================ // 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; } // Widgets with WCLASS_ACCEL_WHEN_HIDDEN (e.g. tab pages) can match // their accel even when invisible, but children are not searched. if (!root->visible) { if (root->wclass && (root->wclass->flags & WCLASS_ACCEL_WHEN_HIDDEN) && 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 (!wclsHas(w, WGT_METHOD_GET_LAYOUT_METRICS)) { return 0; } int32_t pad = 0; int32_t gap = 0; int32_t extraTop = 0; int32_t borderW = 0; wclsGetLayoutMetrics(w, NULL, &pad, &gap, &extraTop, &borderW); return borderW; } // ============================================================ // 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(int32_t type) { if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return false; } return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0; } // ============================================================ // widgetIsBoxContainer // ============================================================ // // Returns true for widget types that use the generic box layout. bool widgetIsBoxContainer(int32_t type) { if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return false; } return (widgetClassTable[type]->flags & WCLASS_BOX_CONTAINER) != 0; } // ============================================================ // widgetIsHorizContainer // ============================================================ // // Returns true for container types that lay out children horizontally. bool widgetIsHorizContainer(int32_t type) { if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return false; } return (widgetClassTable[type]->flags & WCLASS_HORIZ_CONTAINER) != 0; } // ============================================================ // 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; } }