DVX_GUI/dvx/widgets/widgetOps.c

550 lines
16 KiB
C

// 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 "widgetInternal.h"
// ============================================================
// 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;
}
// Paint this widget via vtable
if (w->wclass && w->wclass->paint) {
w->wclass->paint(w, d, ops, font, colors);
}
// Widgets that paint their own children return early
if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) {
if (sDebugLayout) {
debugContainerBorder(w, d, ops);
}
return;
}
// Paint 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 && 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;
}
if (sOpenPopup->wclass && sOpenPopup->wclass->paintOverlay) {
sOpenPopup->wclass->paintOverlay(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;
}
if (w->parent) {
widgetRemoveChild(w->parent, w);
}
widgetDestroyChildren(w);
if (w->wclass && w->wclass->destroy) {
w->wclass->destroy(w);
}
// Clear static references
if (sFocusedWidget == w) {
sFocusedWidget = NULL;
}
if (sOpenPopup == w) {
sOpenPopup = NULL;
}
if (sDragSlider == w) {
sDragSlider = NULL;
}
if (sDrawingCanvas == w) {
sDrawingCanvas = NULL;
}
// If this is the root, clear the window's reference
if (w->window && w->window->widgetRoot == w) {
w->window->widgetRoot = NULL;
}
free(w);
}
// ============================================================
// widgetHashName — djb2 hash for widget name lookup
// ============================================================
//
// The djb2 hash function (Dan Bernstein) is used for fast name-based
// widget lookup. The hash is stored in each widget's nameHash field
// so that wgtFind() can reject non-matches without calling strcmp()
// on every node. In a tree of 100 widgets, this typically reduces
// the search from 100 strcmp calls to 1-2.
static uint32_t widgetHashName(const char *s) {
uint32_t h = 5381;
while (*s) {
h = ((h << 5) + h) + (uint8_t)*s;
s++;
}
return h;
}
// ============================================================
// wgtFindByHash — recursive search with hash fast-reject
// ============================================================
//
// Depth-first search by name with hash pre-screening. The hash
// comparison (integer ==) is much cheaper than strcmp and eliminates
// most non-matches. A full strcmp is only done when hashes match,
// which should be the target widget or a rare collision.
static WidgetT *wgtFindByHash(WidgetT *root, const char *name, uint32_t hash) {
if (root->nameHash == hash && root->name[0] && strcmp(root->name, name) == 0) {
return root;
}
for (WidgetT *c = root->firstChild; c; c = c->nextSibling) {
WidgetT *found = wgtFindByHash(c, name, hash);
if (found) {
return found;
}
}
return NULL;
}
// ============================================================
// wgtFind
// ============================================================
WidgetT *wgtFind(WidgetT *root, const char *name) {
if (!root || !name) {
return NULL;
}
uint32_t hash = widgetHashName(name);
return wgtFindByHash(root, name, hash);
}
// ============================================================
// 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';
w->nameHash = widgetHashName(w->name);
}
// ============================================================
// 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 "";
}
if (w->wclass && w->wclass->getText) {
return w->wclass->getText(w);
}
return "";
}
// ============================================================
// 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 = widgetAlloc(NULL, WidgetVBoxE);
if (!root) {
return NULL;
}
root->window = win;
root->userData = ctx;
win->widgetRoot = root;
win->onPaint = widgetOnPaint;
win->onMouse = widgetOnMouse;
win->onKey = widgetOnKey;
win->onResize = widgetOnResize;
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);
}
// Dirty the window — dvxInvalidateWindow calls onPaint automatically
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;
}
WidgetT *root = w;
while (root->parent) {
root = root->parent;
}
AppContextT *ctx = (AppContextT *)root->userData;
if (!ctx) {
return;
}
// Dirty the window — dvxInvalidateWindow calls onPaint automatically
dvxInvalidateWindow(ctx, w->window);
}
// ============================================================
// wgtPaint
// ============================================================
void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
if (!root) {
return;
}
widgetPaintOne(root, d, ops, font, colors);
}
// ============================================================
// 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;
}
if (sFocusedWidget && sFocusedWidget != w) {
sFocusedWidget->focused = false;
wgtInvalidatePaint(sFocusedWidget);
}
w->focused = true;
sFocusedWidget = w;
wgtInvalidatePaint(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;
}
if (w->wclass && w->wclass->setText) {
w->wclass->setText(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;
wgtInvalidate(w);
}
}