// 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 // widgetOps.c -- Paint dispatcher and public widget operations // // This file contains two categories of functions: // 1. The paint dispatcher (widgetPaintOne, widgetPaintOverlays, wgtPaint) // which walks the widget tree and calls per-type paint functions. // 2. Public operations (wgtSetText, wgtSetEnabled, wgtSetVisible, // wgtFind, wgtDestroy, etc.) that form the widget system's public API. // // The paint dispatcher and the public operations are in the same file // because they share the same invalidation infrastructure (wgtInvalidate // and wgtInvalidatePaint). #include "dvxWgtP.h" #include "dvxPlat.h" #include "stb_ds_wrap.h" #include "../../../widgets/kpunch/box/box.h" static bool sFullRepaint = false; // Knuth multiplicative hash constant (2^32 / golden ratio). #define KNUTH_HASH_MUL 2654435761u // ============================================================ // Prototypes // ============================================================ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops); static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y); static WidgetT *wgtFindImpl(WidgetT *w, const char *name); static bool widgetDropFocusWithin(WidgetT *w); static void widgetNotifyChildChanged(WidgetT *w); // Draws a 1px border in a neon color derived from the widget pointer. // The Knuth multiplicative hash (KNUTH_HASH_MUL) distributes pointer values // across the palette evenly so adjacent containers get different colors. // This is only active when sDebugLayout is true (toggled via // wgtSetDebugLayout), used during development to visualize container // boundaries and diagnose layout issues. static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) { static const uint8_t palette[][3] = { {255, 0, 255}, // magenta { 0, 255, 0}, // lime {255, 255, 0}, // yellow { 0, 255, 255}, // cyan {255, 128, 0}, // orange {128, 0, 255}, // purple {255, 0, 128}, // hot pink { 0, 128, 255}, // sky blue {128, 255, 0}, // chartreuse {255, 64, 64}, // salmon { 64, 255, 128}, // mint {255, 128, 255}, // orchid }; uint32_t h = (uint32_t)(uintptr_t)w * KNUTH_HASH_MUL; int32_t idx = (int32_t)((h >> 16) % DVX_ARRAY_LEN(palette)); uint32_t color = packColor(d, palette[idx][0], palette[idx][1], palette[idx][2]); drawRectOutline(d, ops, w->x, w->y, w->w, w->h, color); } // Destroys a widget and its entire subtree. The order is: // 1. Unlink from parent (so the parent doesn't reference freed memory) // 2. Recursively destroy all children (depth-first) // 3. Call the widget's own destroy callback (free buffers, etc.) // 4. Clear any global state that references this widget // 5. Clear the window's root pointer if this was the root // 6. Free the widget memory // // This ordering ensures that per-widget destroy callbacks can still // access the widget's data (step 3 comes after child cleanup but // before the widget itself is freed). // Shared hit test for pressable widgets: true when (x,y) falls within // the widget bounds and the drag originated in the same window. static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y) { return w->window == root->window && x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h; } void wgtDestroy(WidgetT *w) { if (!w) { return; } // Notify parent chain of child destruction via onChildChanged vtable widgetNotifyChildChanged(w); if (w->parent) { widgetRemoveChild(w->parent, w); } widgetDestroyChildren(w); wclsDestroy(w); widgetClearReferences(w); // If this is the root, clear the window's reference if (w->window && w->window->widgetRoot == w) { w->window->widgetRoot = NULL; } free(w); } // Destroys all children of w, leaving w itself intact and childless. // Each child goes through the full wgtDestroy path -- so parents get // onChildChanged notifications and global references are cleared -- // unlike the internal bulk widgetDestroyChildren used during teardown. // Use this when rebuilding a container's contents at runtime so the // old subtrees are freed instead of leaked. void wgtDestroyChildren(WidgetT *w) { if (!w) { return; } WidgetT *child = w->firstChild; while (child) { WidgetT *next = child->nextSibling; wgtDestroy(child); child = next; } } WidgetT *wgtFind(WidgetT *root, const char *name) { if (!root || !name) { return NULL; } return wgtFindImpl(root, name); } static WidgetT *wgtFindImpl(WidgetT *w, const char *name) { if (w->name[0] && strcmp(w->name, name) == 0) { return w; } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { WidgetT *found = wgtFindImpl(c, name); if (found) { return found; } } return NULL; } // Retrieves the AppContextT from any widget by walking up to the root. // The root widget stores the context in its userData field (set during // wgtInitWindow). This is the only way to get the AppContextT from // deep inside the widget tree without passing it as a parameter // through every function call. The walk is O(depth) but widget trees // are shallow (typically 3-6 levels deep). AppContextT *wgtGetContext(const WidgetT *w) { if (!w) { return NULL; } const WidgetT *root = w; while (root->parent) { root = root->parent; } return (AppContextT *)root->userData; } WidgetT *wgtGetFocused(void) { return sFocusedWidget; } // Polymorphic text getter -- dispatches through the vtable to the // appropriate getText implementation for the widget's type. Returns // an empty string (not NULL) if the widget has no text or no getText // handler, so callers don't need NULL checks. const char *wgtGetText(const WidgetT *w) { if (!w) { return ""; } return wclsGetText(w); } // Sets up a window for widget-based content. Creates a root VBox // container and installs the four window callbacks (onPaint, onMouse, // onKey, onResize) that bridge WM events into the widget system. // // The root widget's userData points to the AppContextT, which is // the bridge back to the display, font, colors, and blitOps needed // for painting. This avoids threading the context through every // widget function -- any widget can retrieve it via wgtGetContext(). WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { WidgetT *root = dvxBoxApi() ? dvxBoxApi()->vBox(NULL) : NULL; if (!root) { dvxLog("Widget: wgtInitWindow failed (%s)", dvxBoxApi() ? "vBox returned NULL" : "dvxBoxApi returned NULL"); return NULL; } root->window = win; root->userData = ctx; win->widgetRoot = root; win->onPaint = widgetOnPaint; win->onMouse = widgetOnMouse; win->onKey = widgetOnKey; win->onKeyUp = widgetOnKeyUp; win->onResize = widgetOnResize; win->onBlur = widgetOnBlur; win->onFocus = widgetOnFocus; return root; } // Full invalidation: re-measures the widget tree, manages scrollbars, // re-lays out, repaints, and dirties the window on screen. // // This is the "something structural changed" path -- use when widget // sizes may have changed (text changed, children added/removed, // visibility toggled). If only visual state changed (cursor blink, // selection highlight), use wgtInvalidatePaint() instead to skip // the expensive measure/layout passes. // // The widgetOnPaint check ensures that custom paint handlers (used // by some dialog implementations) aren't bypassed by the scrollbar // management code. void wgtInvalidate(WidgetT *w) { if (!w || !w->window) { return; } AppContextT *ctx = wgtGetContext(w); if (!ctx) { return; } // Manage scrollbars (measures, adds/removes scrollbars, relayouts) // Skip if window has a custom paint handler (e.g. dialog) that manages its own layout if (w->window->onPaint == widgetOnPaint) { widgetManageScrollbars(w->window, ctx); } // Full repaint — layout changed, all widgets need redrawing w->window->paintNeeded = PAINT_FULL; dvxInvalidateWindow(ctx, w->window); } // Lightweight repaint -- skips measure/layout/scrollbar management. // Use when only visual state changed (slider value, cursor blink, // selection highlight, checkbox toggle) but widget sizes are stable. void wgtInvalidatePaint(WidgetT *w) { if (!w || !w->window) { return; } // Mark only this widget as needing repaint w->paintDirty = true; // Propagate childDirty up through WCLASS_PAINTS_CHILDREN ancestors // so they know to recurse into children during partial repaints. WidgetT *root = w; while (root->parent) { root = root->parent; if (root->wclass && (root->wclass->flags & WCLASS_PAINTS_CHILDREN)) { root->childDirty = true; } } // Defer the actual paint — it will happen once in the main loop // before compositing, batching multiple invalidations into one // tree walk instead of one per call. Don't downgrade FULL to PARTIAL. if (w->window->paintNeeded < PAINT_PARTIAL) { w->window->paintNeeded = PAINT_PARTIAL; } } void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, bool fullRepaint) { if (!root) { return; } sFullRepaint = fullRepaint; widgetPaintOne(root, d, ops, font, colors); sFullRepaint = false; } void wgtSetDebugLayout(AppContextT *ctx, bool enabled) { sDebugLayout = enabled; for (int32_t i = 0; i < ctx->stack.count; i++) { WindowT *win = ctx->stack.windows[i]; if (win->widgetRoot) { wgtInvalidate(win->widgetRoot); } } } void wgtSetEnabled(WidgetT *w, bool enabled) { if (!w) { return; } w->enabled = enabled; // A disabled subtree must not keep keyboard focus. Bail if the blur // callback destroyed widgets -- w may be gone. if (!enabled && !widgetDropFocusWithin(w)) { return; } wgtInvalidatePaint(w); } // Programmatic focus. Disabled or hidden widgets (including children of a // hidden container) cannot take focus; the transition itself is the shared // widgetTransferFocus path. void wgtSetFocused(WidgetT *w) { if (!w || !w->enabled || !widgetIsShown(w)) { return; } widgetTransferFocus(w); } void wgtSetName(WidgetT *w, const char *name) { if (!w || !name) { return; } strncpy(w->name, name, MAX_WIDGET_NAME - 1); w->name[MAX_WIDGET_NAME - 1] = '\0'; } void wgtSetReadOnly(WidgetT *w, bool readOnly) { if (w) { w->readOnly = readOnly; } } // Polymorphic text setter. Dispatches to the type-specific setText // via vtable, then does a full invalidation because changing text // can change the widget's minimum size (triggering relayout). void wgtSetText(WidgetT *w, const char *text) { if (!w) { return; } wclsSetText(w, text); wgtInvalidate(w); } void wgtSetTooltip(WidgetT *w, const char *text) { if (w) { // The on-screen tooltip may borrow the OLD string, whose owner may // free it before reassigning (e.g. a BASIC ToolTipText write); // hide it before the pointer is replaced. if (w->tooltip && w->tooltip != text && w->window && w->window->widgetRoot) { dvxInvalidateTooltip((AppContextT *)w->window->widgetRoot->userData, w->tooltip); } w->tooltip = text; } } void wgtSetVisible(WidgetT *w, bool visible) { if (!w) { return; } w->visible = visible; // A hidden subtree must not keep keyboard focus. Bail if the blur // callback destroyed widgets -- w may be gone. if (!visible && !widgetDropFocusWithin(w)) { return; } // Notify parent chain of child visibility change via onChildChanged vtable widgetNotifyChildChanged(w); wgtInvalidate(w); } // Clears keyboard focus if the focused widget is w or one of its // descendants (used when w is hidden or disabled). Returns false if the // blur callback destroyed widgets, in which case w may be dangling. static bool widgetDropFocusWithin(WidgetT *w) { for (const WidgetT *p = sFocusedWidget; p; p = p->parent) { if (p == w) { return widgetTransferFocus(NULL); } } return true; } // Walks the ancestor chain and notifies the nearest ancestor that implements // onChildChanged of a structural change to w (destroy, visibility toggle). // Stops at the first ancestor that handles it. static void widgetNotifyChildChanged(WidgetT *w) { if (!w->parent) { return; } for (WidgetT *p = w->parent; p; p = p->parent) { if (wclsHas(p, WGT_METHOD_ON_CHILD_CHANGED)) { wclsOnChildChanged(p, w); break; } } } // Recursive paint walker. For each visible widget: // 1. Call the widget's paint function (if any) via vtable. // 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion -- // the widget's paint function already handled its children // (e.g. TabControl only paints the active tab's children). // 3. Otherwise, recurse into children (default child painting). // 4. Draw debug borders on top if debug layout is enabled. // // The paint order is parent-before-children, which means parent // backgrounds are drawn first and children paint on top. This is // the standard painter's algorithm for nested UI elements. void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { if (!w->visible) { return; } // On partial repaints (fullRepaint=false), only paint dirty widgets. // The window's fullRepaint flag is stored in sFullRepaint for the // duration of the paint walk. bool dirty = w->paintDirty || sFullRepaint; // For WCLASS_PAINTS_CHILDREN widgets (TabControl, TreeView, ScrollPane, // Splitter): the generic child recursion below can't reach their // children, so these widgets must handle child painting themselves. // Only call their paint when something actually needs drawing. bool paintsChildren = w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN); if (paintsChildren) { // On full repaint, ensure paintDirty is set so the paint function // redraws its chrome (the window background was cleared). if (sFullRepaint) { w->paintDirty = true; } // Skip entirely if nothing needs painting in this subtree if (!w->paintDirty && !w->childDirty) { return; } // When this widget itself is dirty (will clear its background), // all descendants must repaint on the fresh background. bool savedFull = sFullRepaint; if (w->paintDirty) { sFullRepaint = true; } wclsPaint(w, d, ops, font, colors); sFullRepaint = savedFull; w->paintDirty = false; w->childDirty = false; if (sDebugLayout && dirty) { debugContainerBorder(w, d, ops); } return; } w->paintDirty = false; if (dirty) { wclsPaint(w, d, ops, font, colors); } // Always recurse into children — a clean parent may have dirty children for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { widgetPaintOne(c, d, ops, font, colors); } // Debug: draw container borders on top of children if (sDebugLayout && dirty && w->firstChild) { debugContainerBorder(w, d, ops); } } // Paints popup overlays (open dropdowns/comboboxes) on top of // the widget tree. Called AFTER the main paint pass so popups // always render above all other widgets regardless of tree position. // // Only one popup can be open at a time (tracked by sOpenPopup). // The tree-root ownership check prevents a popup from one window // being painted into a different window's content buffer. void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { if (!sOpenPopup) { return; } // Verify the popup belongs to this widget tree WidgetT *check = sOpenPopup; while (check->parent) { check = check->parent; } if (check != root) { return; } wclsPaintOverlay(sOpenPopup, d, ops, font, colors); } // Accelerator key activation: mimics keyboard press. The matching key-up // event clears sKeyPressedBtn and fires onClick via wclsOnDragEnd. void widgetPressableOnAccelActivate(WidgetT *w, WidgetT *root) { (void)root; w->pressed = true; sKeyPressedBtn = w; wgtInvalidatePaint(w); } // End of drag: clear pressed, fire onClick if released inside bounds. void widgetPressableOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { w->pressed = false; if (pressableHitTest(w, root, x, y)) { if (w->onClick) { // The click handler may destroy this widget (a BASIC Click // handler unloading its own form); only repaint if it didn't. uint32_t gen = sWidgetGen; w->onClick(w); if (sWidgetGen != gen) { return; } } } wgtInvalidatePaint(w); } // Drag update: pressed tracks whether the cursor is still inside bounds, // giving visual feedback as the user drags away or back onto the button. void widgetPressableOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { w->pressed = pressableHitTest(w, root, x, y); wgtInvalidatePaint(w); } // Keyboard activation (Space/Enter): sets pressed and stores the widget // in sKeyPressedBtn so the key-up handler can fire onClick. void widgetPressableOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)mod; if (key == KEY_SPACE || key == KEY_ENTER) { w->pressed = true; sKeyPressedBtn = w; wgtInvalidatePaint(w); } } // Mouse press: take focus, mark pressed, register as the drag widget so // the event dispatcher routes subsequent drag updates/end back here. void widgetPressableOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vx; (void)vy; sFocusedWidget = w; w->pressed = true; sDragWidget = w; } // Shared text getter for widgets whose DataT first field is // `const char *text`. Returns empty string when nothing is set so // callers never need a NULL check. const char *widgetTextGet(const WidgetT *w) { if (!w || !w->data) { return ""; } const char *t = *(const char **)w->data; return t ? t : ""; } // Shared text setter for widgets whose DataT first field is // `const char *text`. Replaces the owned strdup'd string and // recomputes the accelerator from the '&' prefix. The copy is made // BEFORE the old string is freed so wgtSetText(w, wgtGetText(w)) is // safe and a failed strdup keeps the old text instead of blanking it. void widgetTextSet(WidgetT *w, const char *text) { if (!w || !w->data) { return; } const char **slot = (const char **)w->data; char *copy = NULL; if (text) { copy = strdup(text); if (!copy) { dvxLog("Widget: failed to allocate text"); return; } } free((void *)*slot); *slot = copy; w->accelKey = accelParse(copy); }