// 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 "dvxDraw.h" #include "dvxVideo.h" #include "stb_ds_wrap.h" #include #include #include #include #include // ============================================================ // Constants // ============================================================ #define FRM_LINE_MAX 512 #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 DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y); static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y); static const char *resolveTypeName(const char *typeName); static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value); static void rebuildWidgets(DsgnStateT *ds); static void syncWidgetGeom(DsgnControlT *ctrl); // ============================================================ // dsgnCreateDesignWidget // ============================================================ // // 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); } } } // ============================================================ // dsgnAutoName // ============================================================ 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)); } // ============================================================ // dsgnCreateWidgets // ============================================================ 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 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) { parent = ds->form->controls[j].widget; 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); } } } } } // ============================================================ // dsgnIsContainer // ============================================================ bool dsgnIsContainer(const char *typeName) { const char *wgtName = wgtFindByBasName(typeName); if (wgtName) { const WgtIfaceT *iface = wgtGetIface(wgtName); if (iface) { return iface->isContainer; } } return false; } // ============================================================ // dsgnDefaultEvent // ============================================================ 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"; } // ============================================================ // dsgnFree // ============================================================ void dsgnFree(DsgnStateT *ds) { if (ds->form) { arrfree(ds->form->controls); free(ds->form->code); free(ds->form); ds->form = NULL; } } // ============================================================ // dsgnInit // ============================================================ 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; } // ============================================================ // dsgnLoadFrm // ============================================================ 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; bool inForm = false; // 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[FRM_LINE_MAX]; if (lineLen >= FRM_LINE_MAX) { lineLen = FRM_LINE_MAX - 1; } memcpy(line, lineStart, lineLen); line[lineLen] = '\0'; char *trimmed = line; while (*trimmed == ' ' || *trimmed == '\t') { trimmed++; } if (*trimmed == '\0' || *trimmed == '\'') { continue; } if (strncasecmp(trimmed, "VERSION ", 8) == 0) { continue; } // Begin TypeName CtrlName if (strncasecmp(trimmed, "Begin ", 6) == 0) { 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'; while (*rest == ' ' || *rest == '\t') { 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 (inForm) { DsgnControlT ctrl; memset(&ctrl, 0, sizeof(ctrl)); ctrl.index = -1; snprintf(ctrl.name, DSGN_MAX_NAME, "%s", ctrlName); snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); // Set parent from current nesting if (nestDepth > 0) { snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]); } ctrl.width = DEFAULT_CTRL_W; ctrl.height = DEFAULT_CTRL_H; arrput(form->controls, ctrl); curCtrl = &form->controls[arrlen(form->controls) - 1]; // If this is a container, push onto parent stack if (dsgnIsContainer(typeName) && nestDepth < 7) { snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName); nestDepth++; } } continue; } if (strcasecmp(trimmed, "End") == 0) { if (curCtrl) { // If we're closing a container, pop the parent stack if (nestDepth > 0 && strcasecmp(parentStack[nestDepth - 1], curCtrl->name) == 0) { nestDepth--; } curCtrl = NULL; } else { 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 char *eq = strchr(trimmed, '='); if (eq && inForm) { char key[DSGN_MAX_NAME]; 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'; char *vstart = eq + 1; while (*vstart == ' ' || *vstart == '\t') { vstart++; } 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 (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, "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; } } } } ds->form = form; ds->selectedIdx = -1; return true; } // ============================================================ // dsgnNewForm // ============================================================ 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; } // ============================================================ // dsgnOnKey // ============================================================ 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) { arrdel(ds->form->controls, i); if (i < ds->selectedIdx) { ds->selectedIdx--; } } } arrdel(ds->form->controls, ds->selectedIdx); ds->selectedIdx = -1; ds->form->dirty = true; rebuildWidgets(ds); } } // ============================================================ // dsgnOnMouse // ============================================================ 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 ctrl; memset(&ctrl, 0, sizeof(ctrl)); ctrl.index = -1; dsgnAutoName(ds, typeName, ctrl.name, DSGN_MAX_NAME); snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); ctrl.width = DEFAULT_CTRL_W; ctrl.height = DEFAULT_CTRL_H; setPropValue(&ctrl, "Caption", ctrl.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(ctrl.parentName, DSGN_MAX_NAME, "%s", pc->name); parentWidget = pc->widget; break; } } } // Create the live widget if (parentWidget) { ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget); if (ctrl.widget) { ctrl.widget->minW = wgtPixels(ctrl.width); ctrl.widget->minH = wgtPixels(ctrl.height); } } arrput(ds->form->controls, ctrl); ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1; // Set text AFTER arrput so pointers into the array element are 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; } // ============================================================ // dsgnPaint // ============================================================ // // 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. } // ============================================================ // dsgnPaintOverlay // ============================================================ // // 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); } } // ============================================================ // dsgnSaveFrm // ============================================================ // 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); } 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) { 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; } 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 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); } // 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; } // ============================================================ // dsgnSelectedName // ============================================================ 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; } // ============================================================ // getPropValue // ============================================================ 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; } // ============================================================ // hitTestControl // ============================================================ 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; } // ============================================================ // hitTestHandles // ============================================================ 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; } // ============================================================ // resolveTypeName // ============================================================ static const char *resolveTypeName(const char *typeName) { const char *wgtName = wgtFindByBasName(typeName); if (wgtName) { return wgtName; } if (wgtGetApi(typeName)) { return typeName; } return NULL; } // ============================================================ // setPropValue // ============================================================ 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++; } } // ============================================================ // rebuildWidgets // ============================================================ // // 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); } // ============================================================ // syncWidgetGeom // ============================================================ // // 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; }