#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/box/box.h" static bool sFullRepaint = false; // ============================================================ // debugContainerBorder // ============================================================ // // Draws a 1px border in a neon color derived from the widget pointer. // The Knuth multiplicative hash (2654435761) 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 * 2654435761u; int32_t idx = (int32_t)((h >> 16) % 12); uint32_t color = packColor(d, palette[idx][0], palette[idx][1], palette[idx][2]); drawHLine(d, ops, w->x, w->y, w->w, color); drawHLine(d, ops, w->x, w->y + w->h - 1, w->w, color); drawVLine(d, ops, w->x, w->y, w->h, color); drawVLine(d, ops, w->x + w->w - 1, w->y, w->h, color); } // ============================================================ // widgetPaintOne // ============================================================ // // 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); } } // ============================================================ // widgetPaintOverlays // ============================================================ // // 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); } // ============================================================ // wgtDestroy // ============================================================ // // 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). void wgtDestroy(WidgetT *w) { if (!w) { return; } // Notify parent chain of child destruction via onChildChanged vtable if (w->parent) { for (WidgetT *p = w->parent; p; p = p->parent) { if (wclsHas(p, WGT_METHOD_ON_CHILD_CHANGED)) { wclsOnChildChanged(p, w); break; } } } if (w->parent) { widgetRemoveChild(w->parent, w); } widgetDestroyChildren(w); wclsDestroy(w); // Clear static references if (sFocusedWidget == w) { sFocusedWidget = NULL; } if (sOpenPopup == w) { sOpenPopup = NULL; } if (sDragWidget == w) { sDragWidget = NULL; } if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL)) { for (int32_t i = 0; i < sPollWidgetCount; i++) { if (sPollWidgets[i] == w) { arrdel(sPollWidgets, i); sPollWidgetCount = (int32_t)arrlen(sPollWidgets); break; } } } // If this is the root, clear the window's reference if (w->window && w->window->widgetRoot == w) { w->window->widgetRoot = NULL; } free(w); } // ============================================================ // wgtFind // ============================================================ 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; } WidgetT *wgtFind(WidgetT *root, const char *name) { if (!root || !name) { return NULL; } return wgtFindImpl(root, name); } // ============================================================ // wgtSetName // ============================================================ 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'; } // ============================================================ // wgtGetContext // ============================================================ // // 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; } // ============================================================ // wgtGetText // ============================================================ // // 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); } // ============================================================ // wgtInitWindow // ============================================================ // // 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; } // ============================================================ // wgtInvalidate // ============================================================ // // 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; } // Find the root WidgetT *root = w; while (root->parent) { root = root->parent; } AppContextT *ctx = (AppContextT *)root->userData; 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); } // ============================================================ // wgtInvalidatePaint // ============================================================ // // 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; } } // ============================================================ // wgtPaint // ============================================================ 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; } // ============================================================ // wgtSetDebugLayout // ============================================================ 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); } } } // ============================================================ // wgtGetFocused // ============================================================ WidgetT *wgtGetFocused(void) { return sFocusedWidget; } // ============================================================ // wgtSetEnabled // ============================================================ void wgtSetEnabled(WidgetT *w, bool enabled) { if (w) { w->enabled = enabled; wgtInvalidatePaint(w); } } // ============================================================ // wgtSetFocused // ============================================================ void wgtSetFocused(WidgetT *w) { if (!w || !w->enabled) { return; } WidgetT *prev = sFocusedWidget; if (prev && prev != w) { wgtInvalidatePaint(prev); } sFocusedWidget = w; wgtInvalidatePaint(w); if (prev && prev != w && prev->onBlur) { prev->onBlur(prev); } if (w->onFocus) { w->onFocus(w); } } // ============================================================ // wgtSetReadOnly // ============================================================ void wgtSetReadOnly(WidgetT *w, bool readOnly) { if (w) { w->readOnly = readOnly; } } // ============================================================ // wgtSetText // ============================================================ // // 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); } // ============================================================ // wgtSetTooltip // ============================================================ void wgtSetTooltip(WidgetT *w, const char *text) { if (w) { w->tooltip = text; } } // ============================================================ // wgtSetVisible // ============================================================ void wgtSetVisible(WidgetT *w, bool visible) { if (w) { w->visible = visible; // Notify parent chain of child visibility change via onChildChanged vtable if (w->parent) { for (WidgetT *p = w->parent; p; p = p->parent) { if (wclsHas(p, WGT_METHOD_ON_CHILD_CHANGED)) { wclsOnChildChanged(p, w); break; } } } wgtInvalidate(w); } }