DVX_GUI/core/widgetCore.c

632 lines
19 KiB
C

#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 <time.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.
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);
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 (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 (!w->wclass || !w->wclass->getLayoutMetrics) {
return 0;
}
int32_t pad = 0;
int32_t gap = 0;
int32_t extraTop = 0;
int32_t borderW = 0;
w->wclass->getLayoutMetrics(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;
}
}