// ideDesigner.c -- DVX BASIC form designer implementation
//
// WYSIWYG design surface using real DVX widgets positioned
// identically to how the form runtime lays them out. Selection
// handles and grid dots are drawn in the window's onPaint callback.

#include "ideDesigner.h"
#include "../formrt/formrt.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "dvxWm.h"
#include "stb_ds_wrap.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

// ============================================================
// Constants
// ============================================================

#define DEFAULT_FORM_W   400
#define DEFAULT_FORM_H   300
#define DEFAULT_CTRL_W   100
#define DEFAULT_CTRL_H   30
#define MIN_CTRL_SIZE    8


// ============================================================
// Default event for the Form type (not a widget, so not in iface)
// ============================================================

static const char *FORM_DEFAULT_EVENT = "Load";

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

// dsgnCreateDesignWidget is declared in ideDesigner.h (non-static)
static const char  *getPropValue(const DsgnControlT *ctrl, const char *name);
static int32_t      hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
static DsgnHandleE  hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
static void         rebuildWidgets(DsgnStateT *ds);
static const char  *resolveTypeName(const char *typeName);
static int32_t      saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent);
static void         setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
static void         syncWidgetGeom(DsgnControlT *ctrl);


void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) {
    // Look up the name prefix from the widget interface descriptor.
    // Falls back to the type name itself if no prefix is registered.
    const char *prefix  = typeName;
    const char *wgtName = wgtFindByBasName(typeName);

    if (wgtName) {
        const WgtIfaceT *iface = wgtGetIface(wgtName);

        if (iface && iface->namePrefix) {
            prefix = iface->namePrefix;
        }
    }

    int32_t highest   = 0;
    int32_t prefixLen = (int32_t)strlen(prefix);
    int32_t count     = ds->form ? (int32_t)arrlen(ds->form->controls) : 0;

    for (int32_t i = 0; i < count; i++) {
        if (strncasecmp(ds->form->controls[i]->name, prefix, prefixLen) == 0) {
            int32_t num = atoi(ds->form->controls[i]->name + prefixLen);

            if (num > highest) {
                highest = num;
            }
        }
    }

    snprintf(buf, bufSize, "%s%d", prefix, (int)(highest + 1));
}


void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) {
    if (!win || !form) {
        return;
    }

    int32_t menuCount = (int32_t)arrlen(form->menuItems);

    if (menuCount <= 0) {
        return;
    }

    MenuBarT *bar = wmAddMenuBar(win);

    if (!bar) {
        return;
    }

    MenuT *menuStack[8];
    memset(menuStack, 0, sizeof(menuStack));

    for (int32_t i = 0; i < menuCount; i++) {
        const DsgnMenuItemT *mi = &form->menuItems[i];
        bool isSep      = (mi->caption[0] == '-');
        bool isSubParent = (i + 1 < menuCount && form->menuItems[i + 1].level > mi->level);

        if (mi->level == 0) {
            menuStack[0] = wmAddMenu(bar, mi->caption);
        } else if (isSep && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
            wmAddMenuSeparator(menuStack[mi->level - 1]);
        } else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
            menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
        } else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) {
            int32_t id = DSGN_MENU_ID_BASE + i;

            if (mi->radioCheck) {
                wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
            } else if (mi->checked) {
                wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
            } else {
                wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
            }
        }
    }
}


// dsgnCreateFormWindow / dsgnCreateContentBox
// Thin wrappers around formrt functions for backward compatibility.
WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
    return basFormRtCreateContentBox(root, layout);
}


// Create a real DVX widget for design-time display. Mirrors the
// logic in formrt.c createWidget().
WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
    const char *wgtName = resolveTypeName(vbTypeName);

    if (!wgtName) {
        return NULL;
    }

    const void *api = wgtGetApi(wgtName);

    if (!api) {
        return NULL;
    }

    const WgtIfaceT *iface = wgtGetIface(wgtName);
    uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT;

    typedef WidgetT *(*CreateParentFnT)(WidgetT *);
    typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
    typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
    typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t);
    typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t);
    typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
    typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool);

    switch (sig) {
        case WGT_CREATE_PARENT_TEXT: {
            CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
            return fn(parent, "");
        }
        case WGT_CREATE_PARENT_INT: {
            CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
            return fn(parent, iface->createArgs[0]);
        }
        case WGT_CREATE_PARENT_INT_INT: {
            CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api;
            return fn(parent, iface->createArgs[0], iface->createArgs[1]);
        }
        case WGT_CREATE_PARENT_INT_INT_INT: {
            CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api;
            return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]);
        }
        case WGT_CREATE_PARENT_INT_BOOL: {
            CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
            return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]);
        }
        case WGT_CREATE_PARENT_BOOL: {
            CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
            return fn(parent, (bool)iface->createArgs[0]);
        }
        case WGT_CREATE_PARENT_DATA:
            // Image/ImageButton -- cannot auto-create without pixel data
            return NULL;
        default: {
            CreateParentFnT fn = *(CreateParentFnT *)api;
            return fn(parent);
        }
    }
}


WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) {
    return basFormRtCreateFormWindow(ctx, title, layout, resizable, centered, autoSize, width, height, left, top, outRoot, outContentBox);
}


void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
    if (!ds->form || !contentBox) {
        return;
    }

    ds->form->contentBox = contentBox;
    int32_t count = (int32_t)arrlen(ds->form->controls);

    // Two passes: first create all controls (so containers exist),
    // then parent children inside their containers.
    // Pass 1: create all widgets as top-level children
    for (int32_t i = 0; i < count; i++) {
        DsgnControlT *ctrl = ds->form->controls[i];

        if (ctrl->widget) {
            continue;
        }

        // Find the parent widget. For containers with a non-VBox Layout
        // property, create a content box inside so children use the
        // correct layout direction.
        WidgetT *parent = contentBox;

        if (ctrl->parentName[0]) {
            for (int32_t j = 0; j < count; j++) {
                if (j != i && ds->form->controls[j]->widget &&
                    strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) {
                    DsgnControlT *pc = ds->form->controls[j];
                    parent = pc->widget;
                    const char *layout = getPropValue(pc, "Layout");

                    if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) {
                        // Check if we already created a content box inside
                        if (!parent->firstChild || !parent->firstChild->userData ||
                            parent->firstChild->userData != (void *)pc) {
                            WidgetT *box = dsgnCreateContentBox(parent, layout);
                            box->userData = (void *)pc;
                        }

                        parent = parent->firstChild;
                    }

                    break;
                }
            }
        }

        WidgetT *w = dsgnCreateDesignWidget(ctrl->typeName, parent);

        if (!w) {
            continue;
        }

        ctrl->widget = w;
        wgtSetName(w, ctrl->name);

        // Set Caption/Text
        const char *caption = getPropValue(ctrl, "Caption");
        const char *text    = getPropValue(ctrl, "Text");

        if (caption) { wgtSetText(w, caption); }
        if (text)    { wgtSetText(w, text); }

        // Set size hints for the layout engine.
        // minW/minH set the floor; maxW/maxH cap the size.
        if (ctrl->width > 0) {
            w->minW = wgtPixels(ctrl->width);
        }

        if (ctrl->height > 0) {
            w->minH = wgtPixels(ctrl->height);
        }

        if (ctrl->maxWidth > 0) {
            w->maxW = wgtPixels(ctrl->maxWidth);
        }

        if (ctrl->maxHeight > 0) {
            w->maxH = wgtPixels(ctrl->maxHeight);
        }

        w->weight = ctrl->weight;

        // Apply interface properties (Alignment, etc.) from FRM data
        const char *wgtName = wgtFindByBasName(ctrl->typeName);
        const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;

        if (iface) {
            for (int32_t pi = 0; pi < iface->propCount; pi++) {
                const WgtPropDescT *p = &iface->props[pi];

                if (!p->setFn) {
                    continue;
                }

                const char *val = getPropValue(ctrl, p->name);

                if (!val) {
                    continue;
                }

                if (p->type == WGT_IFACE_ENUM && p->enumNames) {
                    for (int32_t en = 0; p->enumNames[en]; en++) {
                        if (strcasecmp(p->enumNames[en], val) == 0) {
                            ((void (*)(WidgetT *, int32_t))p->setFn)(w, en);
                            break;
                        }
                    }
                } else if (p->type == WGT_IFACE_INT) {
                    ((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val));
                } else if (p->type == WGT_IFACE_BOOL) {
                    ((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0);
                } else if (p->type == WGT_IFACE_STRING) {
                    ((void (*)(WidgetT *, const char *))p->setFn)(w, val);
                }
            }
        }
    }
}


const char *dsgnDefaultEvent(const char *typeName) {
    if (strcasecmp(typeName, "Form") == 0) {
        return FORM_DEFAULT_EVENT;
    }

    const char *wgtName = wgtFindByBasName(typeName);

    if (wgtName) {
        const WgtIfaceT *iface = wgtGetIface(wgtName);

        if (iface && iface->defaultEvent) {
            return iface->defaultEvent;
        }
    }

    return "Click";
}


void dsgnFree(DsgnStateT *ds) {
    if (ds->form) {
        for (int32_t i = 0; i < arrlen(ds->form->controls); i++) {
            free(ds->form->controls[i]);
        }
        arrfree(ds->form->controls);
        arrfree(ds->form->menuItems);
        free(ds->form->code);
        free(ds->form);
        ds->form = NULL;
    }
}


void dsgnInit(DsgnStateT *ds, AppContextT *ctx) {
    memset(ds, 0, sizeof(*ds));
    ds->selectedIdx     = -1;
    ds->activeTool[0]   = '\0';
    ds->mode            = DSGN_IDLE;
    ds->activeHandle = HANDLE_NONE;
    ds->ctx          = ctx;
}


bool dsgnIsContainer(const char *typeName) {
    const char *wgtName = wgtFindByBasName(typeName);

    if (wgtName) {
        const WgtIfaceT *iface = wgtGetIface(wgtName);

        if (iface) {
            return iface->isContainer;
        }
    }

    return false;
}


bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
    if (!source || sourceLen <= 0) {
        return false;
    }

    dsgnFree(ds);

    DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));

    if (!form) {
        return false;
    }

    form->controls  = NULL;
    form->width     = DEFAULT_FORM_W;
    form->height    = DEFAULT_FORM_H;
    form->left      = 0;
    form->top       = 0;
    snprintf(form->layout, DSGN_MAX_NAME, "VBox");
    form->centered  = true;
    form->autoSize  = true;
    form->resizable = true;
    snprintf(form->name, DSGN_MAX_NAME, "Form1");
    snprintf(form->caption, DSGN_MAX_TEXT, "Form1");

    DsgnControlT  *curCtrl     = NULL;
    DsgnMenuItemT *curMenuItem = NULL;
    bool inForm = false;
    bool inMenu = false;
    int32_t menuNestDepth = 0;
    int32_t blockDepth    = 0; // Begin/End nesting depth (0 = form level)
    bool    blockIsContainer[BAS_MAX_FRM_NESTING]; // whether each block is a container

    // Parent name stack for nesting (index 0 = form level)
    char parentStack[8][DSGN_MAX_NAME];
    int32_t nestDepth = 0;
    parentStack[0][0] = '\0';

    const char *pos = source;
    const char *end = source + sourceLen;

    while (pos < end) {
        const char *lineStart = pos;

        while (pos < end && *pos != '\n' && *pos != '\r') {
            pos++;
        }

        int32_t lineLen = (int32_t)(pos - lineStart);

        if (pos < end && *pos == '\r') { pos++; }
        if (pos < end && *pos == '\n') { pos++; }

        char line[BAS_MAX_FRM_LINE_LEN];

        if (lineLen >= BAS_MAX_FRM_LINE_LEN) {
            lineLen = BAS_MAX_FRM_LINE_LEN - 1;
        }

        memcpy(line, lineStart, lineLen);
        line[lineLen] = '\0';

        const char *trimmed = dvxSkipWs(line);

        if (*trimmed == '\0' || *trimmed == '\'') {
            continue;
        }

        if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
            // Accept "VERSION DVX x.xx" (native) and "VERSION x.xx" (VB import).
            // Reject VB forms with version > 1.xx (VB4+/VB6 use features we
            // don't support like OLE controls and binary properties).
            const char *ver = trimmed + 8;

            if (strncasecmp(ver, "DVX ", 4) == 0) {
                // Native DVX BASIC form -- always accepted
            } else {
                // VB form -- check version number
                double vbVer = atof(ver);

                if (vbVer > 2.0) {
                    return false; // VB4+ form, not compatible
                }
            }

            continue;
        }

        // Begin TypeName CtrlName
        if (strncasecmp(trimmed, "Begin ", 6) == 0) {
            const char *rest = trimmed + 6;
            char typeName[DSGN_MAX_NAME];
            char ctrlName[DSGN_MAX_NAME];
            int32_t ti = 0;

            while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) {
                typeName[ti++] = *rest++;
            }

            typeName[ti] = '\0';

            rest = dvxSkipWs(rest);

            int32_t ci = 0;

            while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) {
                ctrlName[ci++] = *rest++;
            }

            ctrlName[ci] = '\0';

            if (strcasecmp(typeName, "Form") == 0) {
                snprintf(form->name, DSGN_MAX_NAME, "%s", ctrlName);
                snprintf(form->caption, DSGN_MAX_TEXT, "%s", ctrlName);
                inForm    = true;
                nestDepth = 0;
                curCtrl   = NULL;
            } else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
                DsgnMenuItemT mi;
                memset(&mi, 0, sizeof(mi));
                snprintf(mi.name, DSGN_MAX_NAME, "%s", ctrlName);
                mi.level   = menuNestDepth;
                mi.enabled = true;
                arrput(form->menuItems, mi);
                curMenuItem = &form->menuItems[arrlen(form->menuItems) - 1];
                curCtrl     = NULL; // not a control
                menuNestDepth++;
                inMenu = true;
                if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; }
                blockDepth++;
            } else if (inForm) {
                DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
                cp->index = -1;
                snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName);
                snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);

                // Set parent from current nesting
                if (nestDepth > 0) {
                    snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]);
                }

                cp->width    = DEFAULT_CTRL_W;
                cp->height   = DEFAULT_CTRL_H;
                arrput(form->controls, cp);
                curCtrl = form->controls[arrlen(form->controls) - 1];
                bool isCtrl = dsgnIsContainer(typeName);
                if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; }
                blockDepth++;

                // If this is a container, push onto parent stack
                if (isCtrl && nestDepth < 7) {
                    snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName);
                    nestDepth++;
                }
            }

            continue;
        }

        if (strcasecmp(trimmed, "End") == 0) {
            if (blockDepth > 0) {
                blockDepth--;

                if (inMenu) {
                    menuNestDepth--;
                    curMenuItem = NULL;

                    if (menuNestDepth <= 0) {
                        menuNestDepth = 0;
                        inMenu = false;
                    }
                } else {
                    // If we're closing a container, pop the parent stack
                    if (blockDepth < BAS_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) {
                        nestDepth--;
                    }

                    curCtrl = NULL;
                }
            } else {
                // blockDepth == 0: this is the form's closing End
                inForm = false;

                // Everything after the form's closing End is code
                if (pos < end) {
                    // Skip leading blank lines
                    const char *codeStart = pos;

                    while (codeStart < end && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
                        codeStart++;
                    }

                    if (codeStart < end) {
                        int32_t codeLen = (int32_t)(end - codeStart);
                        form->code = (char *)malloc(codeLen + 1);

                        if (form->code) {
                            memcpy(form->code, codeStart, codeLen);
                            form->code[codeLen] = '\0';
                        }
                    }
                }

                break; // done parsing
            }

            continue;
        }

        // Property = Value
        const char *eq = strchr(trimmed, '=');

        if (eq && inForm) {
            char key[DSGN_MAX_NAME];
            const char *kend = eq - 1;

            while (kend > trimmed && (*kend == ' ' || *kend == '\t')) { kend--; }

            int32_t klen = (int32_t)(kend - trimmed + 1);

            if (klen >= DSGN_MAX_NAME) { klen = DSGN_MAX_NAME - 1; }

            memcpy(key, trimmed, klen);
            key[klen] = '\0';

            const char *vstart = dvxSkipWs(eq + 1);

            char val[DSGN_MAX_TEXT];
            int32_t vi = 0;

            if (*vstart == '"') {
                vstart++;

                while (*vstart && *vstart != '"' && vi < DSGN_MAX_TEXT - 1) {
                    val[vi++] = *vstart++;
                }
            } else {
                while (*vstart && *vstart != '\r' && *vstart != '\n' && vi < DSGN_MAX_TEXT - 1) {
                    val[vi++] = *vstart++;
                }

                while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; }
            }

            val[vi] = '\0';

            if (curMenuItem) {
                if (strcasecmp(key, "Caption") == 0)         { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); }
                else if (strcasecmp(key, "Checked") == 0)    { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
                else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
                else if (strcasecmp(key, "Enabled") == 0)    { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
            } else if (curCtrl) {
                if (strcasecmp(key, "Left") == 0)              { curCtrl->left      = atoi(val); }
                else if (strcasecmp(key, "Top") == 0)          { curCtrl->top       = atoi(val); }
                else if (strcasecmp(key, "MinWidth") == 0 ||
                         strcasecmp(key, "Width") == 0)        { curCtrl->width     = atoi(val); }
                else if (strcasecmp(key, "MinHeight") == 0 ||
                         strcasecmp(key, "Height") == 0)       { curCtrl->height    = atoi(val); }
                else if (strcasecmp(key, "MaxWidth") == 0)  { curCtrl->maxWidth  = atoi(val); }
                else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); }
                else if (strcasecmp(key, "Weight") == 0)    { curCtrl->weight    = atoi(val); }
                else if (strcasecmp(key, "Index") == 0)      { curCtrl->index     = atoi(val); }
                else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(curCtrl->helpTopic, DSGN_MAX_NAME, "%s", val); }
                else if (strcasecmp(key, "TabIndex") == 0)   { /* ignored -- DVX has no tab order */ }
                else { setPropValue(curCtrl, key, val); }
            } else {
                if (strcasecmp(key, "Caption") == 0)       { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); }
                else if (strcasecmp(key, "Layout") == 0)    { strncpy(form->layout, val, DSGN_MAX_NAME - 1); form->layout[DSGN_MAX_NAME - 1] = '\0'; }
                else if (strcasecmp(key, "AutoSize") == 0)  { form->autoSize  = (strcasecmp(val, "True") == 0); }
                else if (strcasecmp(key, "Resizable") == 0) { form->resizable = (strcasecmp(val, "True") == 0); }
                else if (strcasecmp(key, "Centered") == 0)  { form->centered  = (strcasecmp(val, "True") == 0); }
                else if (strcasecmp(key, "Left") == 0)      { form->left   = atoi(val); }
                else if (strcasecmp(key, "Top") == 0)       { form->top    = atoi(val); }
                else if (strcasecmp(key, "Width") == 0)     { form->width  = atoi(val); form->autoSize = false; }
                else if (strcasecmp(key, "Height") == 0)    { form->height = atoi(val); form->autoSize = false; }
                else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", val); }
            }
        }
    }

    ds->form        = form;
    ds->selectedIdx = -1;
    return true;
}


void dsgnNewForm(DsgnStateT *ds, const char *name) {
    dsgnFree(ds);

    DsgnFormT *form = (DsgnFormT *)calloc(1, sizeof(DsgnFormT));
    form->controls  = NULL;
    form->width     = DEFAULT_FORM_W;
    form->height    = DEFAULT_FORM_H;
    form->left      = 0;
    form->top       = 0;
    snprintf(form->layout, DSGN_MAX_NAME, "VBox");
    form->centered  = true;
    form->autoSize  = false;
    form->resizable = true;
    snprintf(form->name, DSGN_MAX_NAME, "%s", name);
    snprintf(form->caption, DSGN_MAX_TEXT, "%s", name);

    ds->form        = form;
    ds->selectedIdx = -1;
}


void dsgnOnKey(DsgnStateT *ds, int32_t key) {
    if (!ds->form) {
        return;
    }

    int32_t count = (int32_t)arrlen(ds->form->controls);

    // Delete key -- remove the selected control and any children
    if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < count) {
        const char *delName = ds->form->controls[ds->selectedIdx]->name;

        // Remove children first (controls whose parentName matches)
        for (int32_t i = count - 1; i >= 0; i--) {
            if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i]->parentName, delName) == 0) {
                free(ds->form->controls[i]);
                arrdel(ds->form->controls, i);

                if (i < ds->selectedIdx) {
                    ds->selectedIdx--;
                }
            }
        }

        free(ds->form->controls[ds->selectedIdx]);
        arrdel(ds->form->controls, ds->selectedIdx);
        ds->selectedIdx = -1;
        ds->form->dirty = true;

        rebuildWidgets(ds);
    }
}


void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
    if (!ds->form) {
        return;
    }

    int32_t ctrlCount = (int32_t)arrlen(ds->form->controls);

    if (drag) {
        if (ds->mode == DSGN_RESIZING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
            DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
            int32_t dx = x - ds->dragStartX;
            int32_t dy = y - ds->dragStartY;

            switch (ds->activeHandle) {
                case HANDLE_E:
                    ctrl->width = ds->dragOrigWidth + dx;
                    break;
                case HANDLE_S:
                    ctrl->height = ds->dragOrigHeight + dy;
                    break;
                case HANDLE_SE:
                    ctrl->width  = ds->dragOrigWidth + dx;
                    ctrl->height = ds->dragOrigHeight + dy;
                    break;
                default:
                    break;
            }

            if (ctrl->width < MIN_CTRL_SIZE)  { ctrl->width = MIN_CTRL_SIZE; }
            if (ctrl->height < MIN_CTRL_SIZE) { ctrl->height = MIN_CTRL_SIZE; }

            syncWidgetGeom(ctrl);
            ds->form->dirty = true;
        } else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
            // Determine if we should swap with a neighbor based on drag direction
            int32_t dy = y - ds->dragStartY;
            DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];

            if (dy > 0 && ctrl->widget) {
                // Dragging down -- swap with next control if past its midpoint
                if (ds->selectedIdx < ctrlCount - 1) {
                    DsgnControlT *next = ds->form->controls[ds->selectedIdx + 1];

                    if (next->widget && y > next->widget->y + next->widget->h / 2) {
                        DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
                        ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1];
                        ds->form->controls[ds->selectedIdx + 1] = tmp;
                        rebuildWidgets(ds);
                        ds->selectedIdx++;
                        ds->dragStartY = y;
                        ds->form->dirty = true;
                    }
                }
            } else if (dy < 0 && ctrl->widget) {
                // Dragging up -- swap with previous control if past its midpoint
                if (ds->selectedIdx > 0) {
                    DsgnControlT *prev = ds->form->controls[ds->selectedIdx - 1];

                    if (prev->widget && y < prev->widget->y + prev->widget->h / 2) {
                        DsgnControlT *tmp = ds->form->controls[ds->selectedIdx];
                        ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1];
                        ds->form->controls[ds->selectedIdx - 1] = tmp;
                        rebuildWidgets(ds);
                        ds->selectedIdx--;
                        ds->dragStartY = y;
                        ds->form->dirty = true;
                    }
                }
            }
        }

        return;
    }

    // Mouse click (not drag) -- end any ongoing operation
    if (ds->mode == DSGN_REORDERING || ds->mode == DSGN_RESIZING) {
        ds->mode = DSGN_IDLE;
        return;
    }

    // Pointer tool: select, start resize or reorder
    if (ds->activeTool[0] == '\0') {
        // Check grab handles of selected control first
        if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
            DsgnHandleE handle = hitTestHandles(ds->form->controls[ds->selectedIdx], x, y);

            if (handle != HANDLE_NONE) {
                DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];
                ds->mode           = DSGN_RESIZING;
                ds->activeHandle   = handle;
                ds->dragStartX     = x;
                ds->dragStartY     = y;
                ds->dragOrigWidth  = ctrl->widget ? ctrl->widget->w : ctrl->width;
                ds->dragOrigHeight = ctrl->widget ? ctrl->widget->h : ctrl->height;
                return;
            }
        }

        // Hit test controls -- click to select, drag to reorder
        int32_t hit = hitTestControl(ds, x, y);

        if (hit >= 0) {
            ds->selectedIdx = hit;
            ds->mode        = DSGN_REORDERING;
            ds->dragStartY  = y;
        } else {
            ds->selectedIdx = -1;
        }

        return;
    }

    // Non-pointer tool: place a new control
    const char *typeName = ds->activeTool;
    DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
    cp->index = -1;
    dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME);
    snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
    cp->width  = DEFAULT_CTRL_W;
    cp->height = DEFAULT_CTRL_H;
    setPropValue(cp, "Caption", cp->name);

    // Determine parent: if click is inside a container, nest there
    WidgetT *parentWidget = ds->form->contentBox;

    for (int32_t i = ctrlCount - 1; i >= 0; i--) {
        DsgnControlT *pc = ds->form->controls[i];

        if (pc->widget && dsgnIsContainer(pc->typeName)) {
            int32_t wx = pc->widget->x;
            int32_t wy = pc->widget->y;
            int32_t ww = pc->widget->w;
            int32_t wh = pc->widget->h;

            if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
                snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name);
                parentWidget = pc->widget;
                break;
            }
        }
    }

    // Create the live widget
    if (parentWidget) {
        cp->widget = dsgnCreateDesignWidget(typeName, parentWidget);

        if (cp->widget) {
            cp->widget->minW = wgtPixels(cp->width);
            cp->widget->minH = wgtPixels(cp->height);
        }
    }

    arrput(ds->form->controls, cp);
    ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1;

    // Set text AFTER arrput so pointers are stable (heap-allocated, so always stable)
    DsgnControlT *stable = ds->form->controls[ds->selectedIdx];

    if (stable->widget) {
        const char *caption = getPropValue(stable, "Caption");
        wgtSetName(stable->widget, stable->name);
        wgtSetText(stable->widget, caption ? caption : stable->name);
    }
    ds->activeTool[0] = '\0';
    ds->mode        = DSGN_IDLE;
    ds->form->dirty = true;
}


// Draw grid dots and selection handles over the live widgets.
// Called from the form window's onPaint callback.
void dsgnPaint(DsgnStateT *ds) {
    if (!ds->form || !ds->ctx) {
        return;
    }

    // Grid dots and selection handles are drawn directly onto
    // the window via the display backbuffer. The live widgets
    // handle their own rendering.

    // Nothing to do here for now -- the onPaint hook in ideMain
    // calls dsgnPaintOverlay with the display pointer.
}


// Draw selection handles on the window's painted surface.
// Called after widgets have painted, using direct display drawing.
void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
    (void)winX;
    (void)winY;

    if (!ds->form || !ds->form->controls || !ds->ctx || !ds->formWin || !ds->formWin->contentBuf) {
        return;
    }

    int32_t count = (int32_t)arrlen(ds->form->controls);

    if (ds->selectedIdx < 0 || ds->selectedIdx >= count) {
        return;
    }

    DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx];

    if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
        return;
    }

    // Draw into the window's content buffer (same coordinate space as widgets)
    DisplayT cd = ds->ctx->display;
    cd.backBuf  = ds->formWin->contentBuf;
    cd.width    = ds->formWin->contentW;
    cd.height   = ds->formWin->contentH;
    cd.pitch    = ds->formWin->contentPitch;
    cd.clipX    = 0;
    cd.clipY    = 0;
    cd.clipW    = ds->formWin->contentW;
    cd.clipH    = ds->formWin->contentH;

    const BlitOpsT *ops = &ds->ctx->blitOps;
    uint32_t black      = packColor(&cd, 0, 0, 0);
    uint32_t gray       = packColor(&cd, 128, 128, 128);

    int32_t cx = ctrl->widget->x;
    int32_t cy = ctrl->widget->y;
    int32_t cw = ctrl->widget->w;
    int32_t ch = ctrl->widget->h;
    int32_t hs = DSGN_HANDLE_SIZE;

    // All 8 handles: NW, N, NE, E, SE, S, SW, W
    int32_t hx[8] = { cx - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw - hs/2, cx + cw/2 - hs/2, cx - hs/2, cx - hs/2 };
    int32_t hy[8] = { cy - hs/2, cy - hs/2, cy - hs/2, cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch - hs/2, cy + ch/2 - hs/2 };

    // E (idx 3), S (idx 5), SE (idx 4) are active (black); rest are inactive (gray)
    for (int32_t i = 0; i < 8; i++) {
        bool active = (i == 3 || i == 4 || i == 5);
        rectFill(&cd, ops, hx[i], hy[i], hs, hs, active ? black : gray);
    }
}


int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
    if (!ds->form || !buf || bufSize <= 0) {
        return -1;
    }

    int32_t pos = 0;

    pos += snprintf(buf + pos, bufSize - pos, "VERSION DVX 1.00\n");
    pos += snprintf(buf + pos, bufSize - pos, "Begin Form %s\n", ds->form->name);
    pos += snprintf(buf + pos, bufSize - pos, "    Caption   = \"%s\"\n", ds->form->caption);
    pos += snprintf(buf + pos, bufSize - pos, "    Layout    = %s\n", ds->form->layout);
    pos += snprintf(buf + pos, bufSize - pos, "    AutoSize  = %s\n", ds->form->autoSize ? "True" : "False");
    pos += snprintf(buf + pos, bufSize - pos, "    Resizable = %s\n", ds->form->resizable ? "True" : "False");
    pos += snprintf(buf + pos, bufSize - pos, "    Centered  = %s\n", ds->form->centered ? "True" : "False");

    if (!ds->form->centered) {
        pos += snprintf(buf + pos, bufSize - pos, "    Left      = %d\n", (int)ds->form->left);
        pos += snprintf(buf + pos, bufSize - pos, "    Top       = %d\n", (int)ds->form->top);
    }

    if (!ds->form->autoSize) {
        pos += snprintf(buf + pos, bufSize - pos, "    Width     = %d\n", (int)ds->form->width);
        pos += snprintf(buf + pos, bufSize - pos, "    Height    = %d\n", (int)ds->form->height);
    }

    if (ds->form->helpTopic[0]) {
        pos += snprintf(buf + pos, bufSize - pos, "    HelpTopic = \"%s\"\n", ds->form->helpTopic);
    }

    // Output menu items as nested Begin Menu blocks
    {
        int32_t menuCount = (int32_t)arrlen(ds->form->menuItems);
        int32_t curLevel  = 0;

        for (int32_t i = 0; i < menuCount; i++) {
            DsgnMenuItemT *mi = &ds->form->menuItems[i];

            // Close blocks back to this item's level
            while (curLevel > mi->level) {
                curLevel--;

                for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
                    buf[pos++] = ' ';
                }

                pos += snprintf(buf + pos, bufSize - pos, "End\n");
            }

            // Indent: (level + 1) * 4 spaces (one extra for being inside Form block)
            for (int32_t p = 0; p < (mi->level + 1) * 4; p++) {
                buf[pos++] = ' ';
            }

            pos += snprintf(buf + pos, bufSize - pos, "Begin Menu %s\n", mi->name);

            // Caption
            for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
                buf[pos++] = ' ';
            }

            pos += snprintf(buf + pos, bufSize - pos, "Caption = \"%s\"\n", mi->caption);

            // Optional properties
            if (mi->checked) {
                for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
                    buf[pos++] = ' ';
                }

                pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n");
            }

            if (mi->radioCheck) {
                for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
                    buf[pos++] = ' ';
                }

                pos += snprintf(buf + pos, bufSize - pos, "RadioCheck = True\n");
            }

            if (!mi->enabled) {
                for (int32_t p = 0; p < (mi->level + 2) * 4; p++) {
                    buf[pos++] = ' ';
                }

                pos += snprintf(buf + pos, bufSize - pos, "Enabled = False\n");
            }

            curLevel = mi->level + 1;
        }

        // Close any remaining open menu blocks
        while (curLevel > 0) {
            curLevel--;

            for (int32_t p = 0; p < (curLevel + 1) * 4; p++) {
                buf[pos++] = ' ';
            }

            pos += snprintf(buf + pos, bufSize - pos, "End\n");
        }
    }

    // Output top-level controls (and recurse into containers)
    pos = saveControls(ds->form, buf, bufSize, pos, "", 1);

    pos += snprintf(buf + pos, bufSize - pos, "End\n");

    // Append code section if present
    if (ds->form->code && ds->form->code[0]) {
        int32_t codeLen = (int32_t)strlen(ds->form->code);
        int32_t avail   = bufSize - pos - 2; // room for \n prefix and \n suffix

        if (avail > 0) {
            buf[pos++] = '\n';

            if (codeLen > avail) {
                codeLen = avail;
            }

            memcpy(buf + pos, ds->form->code, codeLen);
            pos += codeLen;

            // Ensure trailing newline
            if (pos > 0 && buf[pos - 1] != '\n' && pos < bufSize - 1) {
                buf[pos++] = '\n';
            }

            buf[pos] = '\0';
        }
    }

    return pos;
}


const char *dsgnSelectedName(const DsgnStateT *ds) {
    if (!ds->form) {
        return "";
    }

    if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
        return ds->form->controls[ds->selectedIdx]->name;
    }

    return ds->form->name;
}


static const char *getPropValue(const DsgnControlT *ctrl, const char *name) {
    for (int32_t i = 0; i < ctrl->propCount; i++) {
        if (strcasecmp(ctrl->props[i].name, name) == 0) {
            return ctrl->props[i].value;
        }
    }

    return NULL;
}


static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) {
    int32_t count = (int32_t)arrlen(ds->form->controls);

    for (int32_t i = count - 1; i >= 0; i--) {
        const DsgnControlT *ctrl = ds->form->controls[i];

        if (!ctrl->widget || !ctrl->widget->visible) {
            continue;
        }

        // Use the widget's actual laid-out position
        int32_t wx = ctrl->widget->x;
        int32_t wy = ctrl->widget->y;
        int32_t ww = ctrl->widget->w;
        int32_t wh = ctrl->widget->h;

        if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
            return i;
        }
    }

    return -1;
}


static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y) {
    if (!ctrl->widget) {
        return HANDLE_NONE;
    }

    int32_t cx = ctrl->widget->x;
    int32_t cy = ctrl->widget->y;
    int32_t cw = ctrl->widget->w;
    int32_t ch = ctrl->widget->h;
    int32_t hs = DSGN_HANDLE_SIZE;

    // 3 handles: E (right edge), S (bottom edge), SE (corner)
    int32_t hx[HANDLE_COUNT] = { cx + cw - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2 };
    int32_t hy[HANDLE_COUNT] = { cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2 };

    for (int32_t i = 0; i < HANDLE_COUNT; i++) {
        if (x >= hx[i] && x < hx[i] + hs && y >= hy[i] && y < hy[i] + hs) {
            return (DsgnHandleE)i;
        }
    }

    return HANDLE_NONE;
}


// Destroy all live widgets and recreate them in the current
// array order. This is the safest way to reorder since the
// widget tree child list matches the creation order.
static void rebuildWidgets(DsgnStateT *ds) {
    WidgetT *parent = ds->form ? ds->form->contentBox : NULL;

    if (!parent) {
        return;
    }

    // Destroy all existing widget children
    // (wgtDestroy not available, so just unlink and let the window own them)
    parent->firstChild = NULL;
    parent->lastChild  = NULL;

    // Clear widget pointers
    int32_t count = (int32_t)arrlen(ds->form->controls);

    for (int32_t i = 0; i < count; i++) {
        ds->form->controls[i]->widget = NULL;
    }

    // Recreate all widgets in current array order
    dsgnCreateWidgets(ds, parent);
}


static const char *resolveTypeName(const char *typeName) {
    const char *wgtName = wgtFindByBasName(typeName);

    if (wgtName) {
        return wgtName;
    }

    if (wgtGetApi(typeName)) {
        return typeName;
    }

    return NULL;
}


// Write controls at a given nesting level with the specified parent name.
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent) {
    int32_t count = (int32_t)arrlen(form->controls);

    for (int32_t i = 0; i < count; i++) {
        const DsgnControlT *ctrl = form->controls[i];

        // Only output controls whose parent matches
        if (parentName[0] == '\0' && ctrl->parentName[0] != '\0') { continue; }
        if (parentName[0] != '\0' && strcasecmp(ctrl->parentName, parentName) != 0) { continue; }

        // Indent
        char pad[32];
        int32_t padLen = indent * 4;
        if (padLen > 31) { padLen = 31; }
        memset(pad, ' ', padLen);
        pad[padLen] = '\0';

        pos += snprintf(buf + pos, bufSize - pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);

        if (ctrl->index >= 0) {
            pos += snprintf(buf + pos, bufSize - pos, "%s    Index    = %d\n", pad, (int)ctrl->index);
        }

        const char *caption = getPropValue(ctrl, "Caption");
        const char *text    = getPropValue(ctrl, "Text");

        if (caption) { pos += snprintf(buf + pos, bufSize - pos, "%s    Caption  = \"%s\"\n", pad, caption); }
        if (text)    { pos += snprintf(buf + pos, bufSize - pos, "%s    Text     = \"%s\"\n", pad, text); }

        pos += snprintf(buf + pos, bufSize - pos, "%s    Left     = %d\n", pad, (int)ctrl->left);
        pos += snprintf(buf + pos, bufSize - pos, "%s    Top      = %d\n", pad, (int)ctrl->top);
        pos += snprintf(buf + pos, bufSize - pos, "%s    MinWidth = %d\n", pad, (int)ctrl->width);
        pos += snprintf(buf + pos, bufSize - pos, "%s    MinHeight = %d\n", pad, (int)ctrl->height);

        if (ctrl->maxWidth > 0) {
            pos += snprintf(buf + pos, bufSize - pos, "%s    MaxWidth = %d\n", pad, (int)ctrl->maxWidth);
        }

        if (ctrl->maxHeight > 0) {
            pos += snprintf(buf + pos, bufSize - pos, "%s    MaxHeight = %d\n", pad, (int)ctrl->maxHeight);
        }

        if (ctrl->weight > 0) {
            pos += snprintf(buf + pos, bufSize - pos, "%s    Weight   = %d\n", pad, (int)ctrl->weight);
        }

        if (ctrl->helpTopic[0]) {
            pos += snprintf(buf + pos, bufSize - pos, "%s    HelpTopic = \"%s\"\n", pad, ctrl->helpTopic);
        }

        for (int32_t j = 0; j < ctrl->propCount; j++) {
            if (strcasecmp(ctrl->props[j].name, "Caption") == 0) { continue; }
            if (strcasecmp(ctrl->props[j].name, "Text") == 0)    { continue; }

            pos += snprintf(buf + pos, bufSize - pos, "%s    %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value);
        }

        // Save interface properties (Alignment, etc.) read from the live widget
        if (ctrl->widget) {
            const char *wgtName = wgtFindByBasName(ctrl->typeName);
            const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;

            if (iface) {
                for (int32_t j = 0; j < iface->propCount; j++) {
                    const WgtPropDescT *p = &iface->props[j];

                    if (!p->getFn || !p->setFn) {
                        continue;
                    }

                    // Skip if already saved as a custom prop
                    bool already = false;

                    for (int32_t k = 0; k < ctrl->propCount; k++) {
                        if (strcasecmp(ctrl->props[k].name, p->name) == 0) {
                            already = true;
                            break;
                        }
                    }

                    if (already) {
                        continue;
                    }

                    if (p->type == WGT_IFACE_ENUM && p->enumNames) {
                        int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
                        const char *name = NULL;

                        for (int32_t en = 0; p->enumNames[en]; en++) {
                            if (en == v) {
                                name = p->enumNames[en];
                                break;
                            }
                        }

                        if (name) {
                            pos += snprintf(buf + pos, bufSize - pos, "%s    %s = %s\n", pad, p->name, name);
                        }
                    } else if (p->type == WGT_IFACE_INT) {
                        int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
                        pos += snprintf(buf + pos, bufSize - pos, "%s    %s = %d\n", pad, p->name, (int)v);
                    } else if (p->type == WGT_IFACE_BOOL) {
                        bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
                        pos += snprintf(buf + pos, bufSize - pos, "%s    %s = %s\n", pad, p->name, v ? "True" : "False");
                    }
                }
            }
        }

        // Recursively output children of this container
        if (dsgnIsContainer(ctrl->typeName)) {
            pos = saveControls(form, buf, bufSize, pos, ctrl->name, indent + 1);
        }

        pos += snprintf(buf + pos, bufSize - pos, "%sEnd\n", pad);
    }

    return pos;
}


static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value) {
    for (int32_t i = 0; i < ctrl->propCount; i++) {
        if (strcasecmp(ctrl->props[i].name, name) == 0) {
            snprintf(ctrl->props[i].value, DSGN_MAX_TEXT, "%s", value);
            return;
        }
    }

    if (ctrl->propCount < DSGN_MAX_PROPS) {
        snprintf(ctrl->props[ctrl->propCount].name, DSGN_MAX_NAME, "%s", name);
        snprintf(ctrl->props[ctrl->propCount].value, DSGN_MAX_TEXT, "%s", value);
        ctrl->propCount++;
    }
}


// Update the live widget's position and size from the design data.
static void syncWidgetGeom(DsgnControlT *ctrl) {
    if (!ctrl->widget) {
        return;
    }

    ctrl->widget->minW = wgtPixels(ctrl->width);
    ctrl->widget->minH = wgtPixels(ctrl->height);
    ctrl->widget->maxW = ctrl->maxWidth  > 0 ? wgtPixels(ctrl->maxWidth)  : 0;
    ctrl->widget->maxH = ctrl->maxHeight > 0 ? wgtPixels(ctrl->maxHeight) : 0;
}
