// 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 #define DEFAULT_CREATE_ARG 256 // ============================================================ // Tool type name table // ============================================================ static const char *sToolTypeNames[TOOL_COUNT] = { "", // TOOL_POINTER "CommandButton", // TOOL_BUTTON "Label", // TOOL_LABEL "TextBox", // TOOL_TEXTBOX "CheckBox", // TOOL_CHECKBOX "OptionButton", // TOOL_OPTION "Frame", // TOOL_FRAME "ListBox", // TOOL_LISTBOX "ComboBox", // TOOL_COMBOBOX "HScrollBar", // TOOL_HSCROLL "VScrollBar", // TOOL_VSCROLL "Timer", // TOOL_TIMER "PictureBox", // TOOL_PICTURE "Image" // TOOL_IMAGE }; // ============================================================ // Default event table // ============================================================ typedef struct { const char *typeName; const char *eventName; } DefaultEventT; static const DefaultEventT sDefaultEvents[] = { { "CommandButton", "Click" }, { "Label", "Click" }, { "TextBox", "Change" }, { "CheckBox", "Click" }, { "OptionButton", "Click" }, { "Frame", "Click" }, { "ListBox", "Click" }, { "ComboBox", "Click" }, { "Timer", "Timer" }, { "PictureBox", "Click" }, { "Image", "Click" }, { "Form", "Load" }, { NULL, NULL } }; // ============================================================ // Prototypes // ============================================================ static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent); 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); // ============================================================ // createDesignWidget // ============================================================ // // Create a real DVX widget for design-time display. Mirrors the // logic in formrt.c createWidget(). static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) { const char *wgtName = resolveTypeName(vbTypeName); if (!wgtName) { return NULL; } const void *api = wgtGetApi(wgtName); if (!api) { return NULL; } typedef WidgetT *(*CreateParentFnT)(WidgetT *); typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *); typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t); typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); if (strcasecmp(wgtName, "button") == 0 || strcasecmp(wgtName, "checkbox") == 0 || strcasecmp(wgtName, "label") == 0) { CreateParentTextFnT fn = *(CreateParentTextFnT *)api; return fn(parent, ""); } if (strcasecmp(wgtName, "textinput") == 0 || strcasecmp(wgtName, "combobox") == 0) { CreateParentIntFnT fn = *(CreateParentIntFnT *)api; return fn(parent, DEFAULT_CREATE_ARG); } if (strcasecmp(wgtName, "timer") == 0) { CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api; return fn(parent, 1000, false); } if (strcasecmp(wgtName, "listbox") == 0) { CreateParentFnT fn = *(CreateParentFnT *)api; return fn(parent); } // Fallback: try simple create(parent) CreateParentFnT fn = *(CreateParentFnT *)api; return fn(parent); } // ============================================================ // dsgnAutoName // ============================================================ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) { const char *prefix = typeName; if (strcasecmp(typeName, "CommandButton") == 0) { prefix = "Command"; } else if (strcasecmp(typeName, "OptionButton") == 0) { prefix = "Option"; } else if (strcasecmp(typeName, "HScrollBar") == 0) { prefix = "HScroll"; } else if (strcasecmp(typeName, "VScrollBar") == 0) { prefix = "VScroll"; } else if (strcasecmp(typeName, "PictureBox") == 0) { prefix = "Picture"; } 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); for (int32_t i = 0; i < count; i++) { DsgnControlT *ctrl = &ds->form->controls[i]; if (ctrl->widget) { continue; // already created } WidgetT *w = createDesignWidget(ctrl->typeName, contentBox); 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 if (ctrl->height > 0) { w->minH = wgtPixels(ctrl->height); w->prefH = wgtPixels(ctrl->height); } } } // ============================================================ // dsgnDefaultEvent // ============================================================ const char *dsgnDefaultEvent(const char *typeName) { for (int32_t i = 0; sDefaultEvents[i].typeName; i++) { if (strcasecmp(typeName, sDefaultEvents[i].typeName) == 0) { return sDefaultEvents[i].eventName; } } return "Click"; } // ============================================================ // dsgnFree // ============================================================ void dsgnFree(DsgnStateT *ds) { if (ds->form) { arrfree(ds->form->controls); free(ds->form); ds->form = NULL; } } // ============================================================ // dsgnInit // ============================================================ void dsgnInit(DsgnStateT *ds, AppContextT *ctx) { memset(ds, 0, sizeof(*ds)); ds->selectedIdx = -1; ds->activeTool = TOOL_POINTER; 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; snprintf(form->name, DSGN_MAX_NAME, "Form1"); snprintf(form->caption, DSGN_MAX_TEXT, "Form1"); DsgnControlT *curCtrl = NULL; bool inForm = false; 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; curCtrl = NULL; } else if (inForm) { DsgnControlT ctrl; memset(&ctrl, 0, sizeof(ctrl)); snprintf(ctrl.name, DSGN_MAX_NAME, "%s", ctrlName); snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); ctrl.width = DEFAULT_CTRL_W; ctrl.height = DEFAULT_CTRL_H; ctrl.tabIndex = (int32_t)arrlen(form->controls); arrput(form->controls, ctrl); curCtrl = &form->controls[arrlen(form->controls) - 1]; } continue; } if (strcasecmp(trimmed, "End") == 0) { if (curCtrl) { curCtrl = NULL; } else { inForm = false; } 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, "Width") == 0) { curCtrl->width = atoi(val); } else if (strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); } else if (strcasecmp(key, "TabIndex") == 0) { curCtrl->tabIndex = atoi(val); } else { setPropValue(curCtrl, key, val); } } else { if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); } else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); } else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); } } } } 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; 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; } // Delete key if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { arrdel(ds->form->controls, ds->selectedIdx); ds->selectedIdx = -1; ds->form->dirty = true; } } // ============================================================ // 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 == TOOL_POINTER) { // 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 (appended to VBox) const char *typeName = dsgnToolTypeName(ds->activeTool); DsgnControlT ctrl; memset(&ctrl, 0, sizeof(ctrl)); 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; ctrl.tabIndex = ctrlCount; setPropValue(&ctrl, "Caption", ctrl.name); // Create the live widget if (ds->form->contentBox) { ctrl.widget = createDesignWidget(typeName, ds->form->contentBox); if (ctrl.widget) { wgtSetName(ctrl.widget, ctrl.name); wgtSetText(ctrl.widget, ctrl.name); ctrl.widget->prefW = wgtPixels(ctrl.width); ctrl.widget->prefH = wgtPixels(ctrl.height); } } arrput(ds->form->controls, ctrl); ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1; ds->activeTool = TOOL_POINTER; 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->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); 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++) { rectFill(&cd, ops, hx[i], hy[i], hs, hs, black); } } // ============================================================ // dsgnSaveFrm // ============================================================ 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, " Width = %d\n", (int)ds->form->width); pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ds->form->height); int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < count; i++) { const DsgnControlT *ctrl = &ds->form->controls[i]; pos += snprintf(buf + pos, bufSize - pos, " Begin %s %s\n", ctrl->typeName, ctrl->name); const char *caption = getPropValue(ctrl, "Caption"); const char *text = getPropValue(ctrl, "Text"); if (caption) { pos += snprintf(buf + pos, bufSize - pos, " Caption = \"%s\"\n", caption); } if (text) { pos += snprintf(buf + pos, bufSize - pos, " Text = \"%s\"\n", text); } pos += snprintf(buf + pos, bufSize - pos, " Left = %d\n", (int)ctrl->left); pos += snprintf(buf + pos, bufSize - pos, " Top = %d\n", (int)ctrl->top); pos += snprintf(buf + pos, bufSize - pos, " Width = %d\n", (int)ctrl->width); pos += snprintf(buf + pos, bufSize - pos, " Height = %d\n", (int)ctrl->height); 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\"\n", ctrl->props[j].name, ctrl->props[j].value); } pos += snprintf(buf + pos, bufSize - pos, " End\n"); } pos += snprintf(buf + pos, bufSize - pos, "End\n"); 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; } // ============================================================ // dsgnToolTypeName // ============================================================ const char *dsgnToolTypeName(DsgnToolE tool) { if (tool >= 0 && tool < TOOL_COUNT) { return sToolTypeNames[tool]; } return ""; } // ============================================================ // 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) { 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; } // Set minW/minH -- the layout engine uses these as the floor // during the measure pass, so the arrange pass allocates this // exact size (assuming weight=0). ctrl->widget->minH = wgtPixels(ctrl->height); ctrl->widget->prefH = wgtPixels(ctrl->height); }