688 lines
20 KiB
C
688 lines
20 KiB
C
// 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);
|
|
}
|