// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#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 "dvxWgtP.h"
#include "dvxDraw.h"
#include "dvxPlat.h"
#include "stb_ds_wrap.h"

#include <stdlib.h>
#include <string.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)
WidgetT **sPollWidgets    = NULL;   // stb_ds dynamic array
uint32_t  sWidgetGen      = 0;      // bumped per widget destroy (see dvxWgtP.h)

// Widget-destroy subscribers.  A DXE that keeps a static WidgetT pointer
// (e.g. texthelp's selection tracker) registers a callback here so its
// static is nulled before the widget is freed.  A subscriber LIST rather
// than a single slot means a second registrant cannot silently clobber a
// first, reviving the use-after-free the hook exists to prevent.
static void (**sWidgetDestroyFns)(WidgetT *w) = NULL;   // stb_ds dynamic array

// Shared clipboard -- process-wide, not per-widget.
static char    *sClipboard    = NULL;
static int32_t  sClipboardLen = 0;
static int32_t  sClipboardCap = 0;

// Max pixel movement between clicks that still counts as a double-click.
#define DBLCLICK_TOLERANCE 4

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


// ============================================================
// Prototypes
// ============================================================

static void     collectFocusable(WidgetT *w, WidgetT ***list);
static WidgetT *findNextFocusableImpl(WidgetT *w, WidgetT *after, bool *pastAfter);


void clipboardCopy(const char *text, int32_t len) {
    if (!text || len <= 0) {
        return;
    }

    if (len + 1 > sClipboardCap) {
        int32_t newCap = len + 1;
        char *newBuf   = (char *)realloc(sClipboard, newCap);

        if (!newBuf) {
            return;
        }

        sClipboard    = newBuf;
        sClipboardCap = newCap;
    }

    memcpy(sClipboard, text, len);
    sClipboard[len] = '\0';
    sClipboardLen   = len;
}


const char *clipboardGet(int32_t *outLen) {
    if (outLen) {
        *outLen = sClipboardLen;
    }

    return sClipboard;
}


// Appends every focusable widget in the subtree rooted at w to *list in
// depth-first (Tab) order, skipping hidden/disabled subtrees.
static void collectFocusable(WidgetT *w, WidgetT ***list) {
    if (!w->visible || !w->enabled) {
        return;
    }

    if (widgetIsFocusable(w->type)) {
        arrput(*list, w);
    }

    for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
        collectFocusable(c, list);
    }
}


// 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) {
    // Mark 'after' as passed BEFORE the visibility gate so a widget that was
    // hidden or disabled while focused still anchors the search; otherwise
    // Tab would wrap to the first widget instead of the next one.
    if (after == NULL || w == after) {
        *pastAfter = true;
    }

    if (!w->visible || !w->enabled) {
        return NULL;
    }

    if (w != after && *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;
}


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) < DBLCLICK_TOLERANCE && abs(vy - sLastClickY) < DBLCLICK_TOLERANCE) {
        sClickCount++;
    } else {
        sClickCount = 1;
    }

    sLastClickTime = now;
    sLastClickX    = vx;
    sLastClickY    = vy;

    return sClickCount;
}


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


// 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) {
        dvxLog("Widget: failed to allocate widget (type=%d)", type);
        return NULL;
    }

    memset(w, 0, sizeof(*w));
    w->type    = type;
    w->wclass  = widgetClassTable[type];
    w->visible = true;
    w->enabled = true;

    if (w->wclass->flags & WCLASS_NEEDS_POLL) {
        arrput(sPollWidgets, w);
    }

    if (parent) {
        w->window = parent->window;
        widgetAddChild(parent, w);
    }

    return w;
}


// Undo a successful widgetAlloc for a widget whose data-struct
// allocation subsequently failed.  Detaches from parent, removes
// from the poll list, and frees the widget struct.  Safe only
// before w->data has been set -- once data is attached, use
// wgtDestroy to run the widget's full teardown path.
void widgetAllocRollback(WidgetT *w) {
    if (!w) {
        return;
    }

    if (w->parent) {
        widgetRemoveChild(w->parent, w);
    }

    widgetClearReferences(w);

    free(w);
}


WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text) {
    WidgetT *w = widgetAlloc(parent, type);

    if (!w) {
        return NULL;
    }

    void *data = calloc(1, dataSize);

    if (!data) {
        dvxLog("Widget: failed to allocate %u-byte data for type %d", (unsigned)dataSize, type);
        widgetAllocRollback(w);
        return NULL;
    }

    // The data struct must begin with a `const char *text` field.
    *(const char **)data = text ? strdup(text) : NULL;
    w->data     = data;
    w->accelKey = accelParse(text);
    return w;
}


// Clears every global interaction pointer (focus/popup/drag/key-pressed)
// that references w and removes w from the poll list.  Must be called for
// every destroyed node so no global is left pointing at freed memory.
// Also bumps sWidgetGen so in-flight dispatch code knows its cached
// widget pointers may now be dangling (see dvxWgtP.h).
void widgetClearReferences(WidgetT *w) {
    sWidgetGen++;

    if (sFocusedWidget == w) {
        sFocusedWidget = NULL;
    }

    if (sOpenPopup == w) {
        sOpenPopup = NULL;
    }

    if (sClosedPopup == w) {
        sClosedPopup = NULL;
    }

    if (sDragWidget == w) {
        sDragWidget = NULL;
    }

    if (sKeyPressedBtn == w) {
        sKeyPressedBtn = NULL;
    }

    if (w->window && w->window->lastFocusWidget == w) {
        w->window->lastFocusWidget = NULL;
    }

    // The on-screen tooltip may borrow w->tooltip, which dies with the
    // widget; hide it so the compositor stops drawing a freed string.
    // The context lives in the window root's userData (see wgtInitWindow);
    // w itself may already be detached from the tree at this point.
    if (w->tooltip && w->window && w->window->widgetRoot) {
        dvxInvalidateTooltip((AppContextT *)w->window->widgetRoot->userData, w->tooltip);
    }

    // Notify every DXE-registered subscriber (e.g. texthelp's selection
    // tracker) so foreign static pointers to w are nulled before w is freed.
    // arrlen is re-read each iteration so a subscriber that unregisters one
    // mid-walk cannot index past the array.
    for (int32_t i = 0; i < (int32_t)arrlen(sWidgetDestroyFns); i++) {
        if (sWidgetDestroyFns[i]) {
            sWidgetDestroyFns[i](w);
        }
    }

    if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) {
        for (int32_t i = 0; i < (int32_t)arrlen(sPollWidgets); i++) {
            if (sPollWidgets[i] == w) {
                arrdel(sPollWidgets, i);
                break;
            }
        }
    }
}


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


// 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, every destroyed node is passed through
// widgetClearReferences() so that no global interaction pointer (focus,
// popup, drag, key-pressed) and no poll-list entry is left dangling at
// freed memory.

void widgetDestroyChildren(WidgetT *w) {
    WidgetT *child = w->firstChild;

    while (child) {
        WidgetT *next = child->nextSibling;
        widgetDestroyChildren(child);

        wclsDestroy(child);

        widgetClearReferences(child);

        free(child);
        child = next;
    }

    w->firstChild = NULL;
    w->lastChild  = NULL;
}


// Clears every global interaction pointer (focus/popup/drag/key-pressed)
// whose widget lives on this window.  Companion to widgetClearReferences
// for DEFERRED window destruction: the window's widget tree stays
// allocated until the pending-destroy flush, but its backing app state
// (e.g. BASIC control structs in userData) may already be freed, so
// drag/popup/key-press dispatch into it must be cut off immediately.
void widgetDetachWindowReferences(WindowT *win) {
    if (sFocusedWidget && sFocusedWidget->window == win) {
        sFocusedWidget = NULL;
    }

    if (sOpenPopup && sOpenPopup->window == win) {
        wclsClosePopup(sOpenPopup);
        sOpenPopup = NULL;
    }

    if (sClosedPopup && sClosedPopup->window == win) {
        sClosedPopup = NULL;
    }

    if (sDragWidget && sDragWidget->window == win) {
        sDragWidget = NULL;
    }

    if (sKeyPressedBtn && sKeyPressedBtn->window == win) {
        sKeyPressedBtn = NULL;
    }

    win->lastFocusWidget = NULL;
}


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


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


// Shift+Tab navigation: finds the previous focusable widget.
// Collects all focusable widgets via a recursive DFS into one
// stb_ds array, then returns the one before 'before' (with
// wraparound).  A 'before' that is not in the list (hidden or
// disabled while focused) wraps to the last focusable widget.

WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) {
    WidgetT **list = NULL;

    collectFocusable(root, &list);

    WidgetT *result = NULL;
    int32_t  count  = arrlen(list);

    if (count > 0) {
        int32_t idx = -1;

        for (int32_t i = 0; i < count; i++) {
            if (list[i] == before) {
                idx = i;
                break;
            }
        }

        result = (idx <= 0) ? list[count - 1] : list[idx - 1];
    }

    arrfree(list);
    return result;
}


// Fires the focus-transition callbacks for a change that has ALREADY been
// recorded in sFocusedWidget: the class-level blur (commit/clamp edits)
// and app onBlur on prev, then the app onFocus on next.  This is the one
// place the blur/focus sequence lives; every focus path (mouse, Tab,
// accelerator, wgtSetFocused, window blur, paint-time restore) ends here.
//
// Returns false if a callback destroyed widgets (sWidgetGen changed) --
// prev and next may be dangling then and the caller must not touch them.

bool widgetFireFocusChange(WidgetT *prev, WidgetT *next) {
    uint32_t gen = sWidgetGen;

    if (prev && prev != next) {
        // Commit/clamp any in-progress edit before the app onBlur so a
        // LostFocus handler sees the committed value.
        wclsOnBlur(prev);

        if (sWidgetGen == gen && prev->onBlur) {
            prev->onBlur(prev);
        }

        if (sWidgetGen != gen) {
            return false;
        }
    }

    if (next && next != prev && next->onFocus) {
        next->onFocus(next);
    }

    return sWidgetGen == gen;
}


int32_t widgetFrameBorderWidth(const WidgetT *w) {
    int32_t pad;
    int32_t gap;
    int32_t extraTop;
    int32_t borderW;

    widgetBoxMetrics(w, NULL, &pad, &gap, &extraTop, &borderW);

    return borderW;
}


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


bool widgetIsFocusable(int32_t type) {
    if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) {
        return false;
    }

    return (widgetClassTable[type]->flags & WCLASS_FOCUSABLE) != 0;
}


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


// True when w and every ancestor are visible, i.e. the widget can actually
// be seen (a child of a hidden container is hidden even if its own flag is
// set).  Used to keep keyboard focus off widgets the user cannot see.

bool widgetIsShown(const WidgetT *w) {
    for (const WidgetT *p = w; p; p = p->parent) {
        if (!p->visible) {
            return false;
        }
    }

    return true;
}


// Register a widget-destroy subscriber.  Idempotent: a callback already
// present is not added twice, so a module can register on every attach
// without tracking its own state.  Fired by widgetClearReferences for
// every destroyed widget so subscribers can null any static pointer to it.
void widgetRegisterDestroyFn(void (*fn)(WidgetT *w)) {
    if (!fn) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sWidgetDestroyFns); i++) {
        if (sWidgetDestroyFns[i] == fn) {
            return;
        }
    }

    arrput(sWidgetDestroyFns, fn);
}


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


// 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) {
    // No track or no content: no thumb.  Callers need no guard of their own.
    if (trackLen <= 0 || totalSize <= 0) {
        *thumbPos  = 0;
        *thumbSize = 0;
        return;
    }

    *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) * clampInt(scrollPos, 0, maxScroll)) / maxScroll;
    } else {
        *thumbPos = 0;
    }
}


// Map a thumb drag to a new scroll position.  relMouse is the mouse offset
// (from the track origin) minus the grab offset within the thumb.  The
// thumb travels (trackLen - thumbSize) pixels to cover maxScroll units, so
// scroll = maxScroll * relMouse / (trackLen - thumbSize), clamped to range.
// This is the single canonical form shared by every scrolling widget.
int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll) {
    if (maxScroll <= 0) {
        return 0;
    }

    int32_t thumbPos  = 0;
    int32_t thumbSize = 0;

    widgetScrollbarThumb(trackLen, total, visible, 0, &thumbPos, &thumbSize);

    int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0;

    return clampInt(newScroll, 0, maxScroll);
}


// Moves keyboard focus to w (NULL clears focus).  Clears the previous
// widget's selection, repaints both widgets, and fires the blur/focus
// callbacks via widgetFireFocusChange.  A no-op when w already has focus.
//
// Returns false if a callback destroyed widgets -- the caller must not
// dereference w or the previously focused widget afterwards.

bool widgetTransferFocus(WidgetT *w) {
    WidgetT *prev = sFocusedWidget;

    if (prev == w) {
        return true;
    }

    // Switch focus BEFORE invalidating so paint sees the correct focused
    // state for both widgets.
    sFocusedWidget = w;

    if (prev) {
        wclsClearSelection(prev);
        wgtInvalidatePaint(prev);
    }

    if (w) {
        wgtInvalidatePaint(w);
    }

    return widgetFireFocusChange(prev, w);
}


// Remove a widget-destroy subscriber.  No-op if the callback is absent, so
// a double-unregister is safe.
void widgetUnregisterDestroyFn(void (*fn)(WidgetT *w)) {
    for (int32_t i = 0; i < (int32_t)arrlen(sWidgetDestroyFns); i++) {
        if (sWidgetDestroyFns[i] == fn) {
            arrdel(sWidgetDestroyFns, i);
            return;
        }
    }
}
