// ideDesigner.c -- DVX BASIC form designer implementation // // Design surface rendering, hit testing, mouse interaction, // and .frm file I/O. Controls are stored as pure data and // rendered onto a Canvas widget. #include "ideDesigner.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 // ============================================================ // 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 void drawControl(DsgnStateT *ds, const DsgnControlT *ctrl, bool selected); static void drawGrid(DsgnStateT *ds); static void drawHandles(DsgnStateT *ds, const DsgnControlT *ctrl); 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 int32_t snapToGrid(int32_t val, int32_t gridSize); static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value); // ============================================================ // dsgnAutoName // ============================================================ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) { // Map VB type to short prefix 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)); } // ============================================================ // 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->showGrid = true; ds->snapToGrid = true; 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) { // Extract one line 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'; // Trim leading whitespace char *trimmed = line; while (*trimmed == ' ' || *trimmed == '\t') { trimmed++; } if (*trimmed == '\0' || *trimmed == '\'') { continue; } // VERSION line -- skip 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; } // End if (strcasecmp(trimmed, "End") == 0) { if (curCtrl) { curCtrl = NULL; } else { inForm = false; } continue; } // Property = Value char *eq = strchr(trimmed, '='); if (eq && inForm) { // Extract key 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'; // Extract value (skip leading whitespace and quotes) 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++; } // Trim trailing whitespace while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; } } val[vi] = '\0'; // Assign to form or control 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); } } } } // Auto-layout controls that have no position (all at 0,0) int32_t ctrlCount = (int32_t)arrlen(form->controls); bool allAtOrigin = true; for (int32_t i = 0; i < ctrlCount; i++) { if (form->controls[i].left != 0 || form->controls[i].top != 0) { allAtOrigin = false; break; } } if (allAtOrigin && ctrlCount > 0) { int32_t y = DSGN_GRID_SIZE * 2; for (int32_t i = 0; i < ctrlCount; i++) { form->controls[i].left = DSGN_GRID_SIZE * 2; form->controls[i].top = y; y += form->controls[i].height + DSGN_GRID_SIZE; } } 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 -- remove selected control 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; dsgnPaint(ds); } } // ============================================================ // dsgnOnMouse // ============================================================ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { if (!ds->form || !ds->canvas) { return; } int32_t ctrlCount = (int32_t)arrlen(ds->form->controls); if (drag) { // Continue ongoing drag operation if (ds->mode == DSGN_MOVING && 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; ctrl->left = ds->dragOrigLeft + dx; ctrl->top = ds->dragOrigTop + dy; if (ds->snapToGrid) { ctrl->left = snapToGrid(ctrl->left, DSGN_GRID_SIZE); ctrl->top = snapToGrid(ctrl->top, DSGN_GRID_SIZE); } ds->form->dirty = true; dsgnPaint(ds); } else 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; // Apply delta based on which handle switch (ds->activeHandle) { case HANDLE_NW: ctrl->left = ds->dragOrigLeft + dx; ctrl->top = ds->dragOrigTop + dy; ctrl->width = ds->dragOrigWidth - dx; ctrl->height = ds->dragOrigHeight - dy; break; case HANDLE_N: ctrl->top = ds->dragOrigTop + dy; ctrl->height = ds->dragOrigHeight - dy; break; case HANDLE_NE: ctrl->top = ds->dragOrigTop + dy; ctrl->width = ds->dragOrigWidth + dx; ctrl->height = ds->dragOrigHeight - dy; break; case HANDLE_E: ctrl->width = ds->dragOrigWidth + dx; break; case HANDLE_SE: ctrl->width = ds->dragOrigWidth + dx; ctrl->height = ds->dragOrigHeight + dy; break; case HANDLE_S: ctrl->height = ds->dragOrigHeight + dy; break; case HANDLE_SW: ctrl->left = ds->dragOrigLeft + dx; ctrl->width = ds->dragOrigWidth - dx; ctrl->height = ds->dragOrigHeight + dy; break; case HANDLE_W: ctrl->left = ds->dragOrigLeft + dx; ctrl->width = ds->dragOrigWidth - dx; 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; } if (ds->snapToGrid) { ctrl->left = snapToGrid(ctrl->left, DSGN_GRID_SIZE); ctrl->top = snapToGrid(ctrl->top, DSGN_GRID_SIZE); ctrl->width = snapToGrid(ctrl->width, DSGN_GRID_SIZE); ctrl->height = snapToGrid(ctrl->height, DSGN_GRID_SIZE); if (ctrl->width < MIN_CTRL_SIZE) { ctrl->width = MIN_CTRL_SIZE; } if (ctrl->height < MIN_CTRL_SIZE) { ctrl->height = MIN_CTRL_SIZE; } } ds->form->dirty = true; dsgnPaint(ds); } else if (ds->mode == DSGN_DRAWING) { // Rubber-band for new control -- just repaint with preview dsgnPaint(ds); // Draw rubber-band rectangle int32_t rx = ds->drawX < x ? ds->drawX : x; int32_t ry = ds->drawY < y ? ds->drawY : y; int32_t rw = abs(x - ds->drawX); int32_t rh = abs(y - ds->drawY); if (rw > 0 && rh > 0) { uint32_t black = packColor(&ds->ctx->display, 0, 0, 0); wgtCanvasSetPenColor(ds->canvas, black); wgtCanvasDrawRect(ds->canvas, rx, ry, rw, rh); } } return; } // Mouse click (not drag) // If drawing a new control, finalize it if (ds->mode == DSGN_DRAWING) { int32_t rx = ds->drawX < x ? ds->drawX : x; int32_t ry = ds->drawY < y ? ds->drawY : y; int32_t rw = abs(x - ds->drawX); int32_t rh = abs(y - ds->drawY); if (rw < MIN_CTRL_SIZE) { rw = DEFAULT_CTRL_W; } if (rh < MIN_CTRL_SIZE) { rh = DEFAULT_CTRL_H; } if (ds->snapToGrid) { rx = snapToGrid(rx, DSGN_GRID_SIZE); ry = snapToGrid(ry, DSGN_GRID_SIZE); rw = snapToGrid(rw, DSGN_GRID_SIZE); rh = snapToGrid(rh, DSGN_GRID_SIZE); if (rw < MIN_CTRL_SIZE) { rw = MIN_CTRL_SIZE; } if (rh < MIN_CTRL_SIZE) { rh = MIN_CTRL_SIZE; } } 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.left = rx; ctrl.top = ry; ctrl.width = rw; ctrl.height = rh; ctrl.tabIndex = ctrlCount; // Set default Caption/Text setPropValue(&ctrl, "Caption", ctrl.name); 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(ds); return; } // Release from moving/resizing if (ds->mode == DSGN_MOVING || ds->mode == DSGN_RESIZING) { ds->mode = DSGN_IDLE; dsgnPaint(ds); return; } // Placing mode: start drawing a new control if (ds->activeTool != TOOL_POINTER) { ds->mode = DSGN_DRAWING; ds->drawX = x; ds->drawY = y; return; } // Pointer tool: select and start drag // 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->dragOrigLeft = ctrl->left; ds->dragOrigTop = ctrl->top; ds->dragOrigWidth = ctrl->width; ds->dragOrigHeight = ctrl->height; return; } } // Hit test controls int32_t hit = hitTestControl(ds, x, y); if (hit >= 0) { ds->selectedIdx = hit; DsgnControlT *ctrl = &ds->form->controls[hit]; ds->mode = DSGN_MOVING; ds->dragStartX = x; ds->dragStartY = y; ds->dragOrigLeft = ctrl->left; ds->dragOrigTop = ctrl->top; } else { ds->selectedIdx = -1; } dsgnPaint(ds); } // ============================================================ // dsgnPaint // ============================================================ void dsgnPaint(DsgnStateT *ds) { if (!ds->canvas || !ds->form || !ds->ctx) { return; } // Form background uint32_t formBg = packColor(&ds->ctx->display, 192, 192, 192); wgtCanvasClear(ds->canvas, formBg); // Grid dots if (ds->showGrid) { drawGrid(ds); } // Draw controls int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < count; i++) { drawControl(ds, &ds->form->controls[i], i == ds->selectedIdx); } wgtInvalidatePaint(ds->canvas); } // ============================================================ // 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); // Write Caption/Text first if present 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); } // Geometry 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); // Other properties (skip Caption, Text, geometry -- already written) 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; } // ============================================================ // dsgnSetCanvas // ============================================================ void dsgnSetCanvas(DsgnStateT *ds, WidgetT *canvas) { ds->canvas = canvas; } // ============================================================ // dsgnToolTypeName // ============================================================ const char *dsgnToolTypeName(DsgnToolE tool) { if (tool >= 0 && tool < TOOL_COUNT) { return sToolTypeNames[tool]; } return ""; } // ============================================================ // drawControl // ============================================================ static void drawControl(DsgnStateT *ds, const DsgnControlT *ctrl, bool selected) { WidgetT *cv = ds->canvas; int32_t x = ctrl->left; int32_t y = ctrl->top; int32_t w = ctrl->width; int32_t h = ctrl->height; uint32_t black = packColor(&ds->ctx->display, 0, 0, 0); uint32_t face = packColor(&ds->ctx->display, 192, 192, 192); uint32_t hilight = packColor(&ds->ctx->display, 255, 255, 255); uint32_t shadow = packColor(&ds->ctx->display, 128, 128, 128); uint32_t dkShadow = packColor(&ds->ctx->display, 64, 64, 64); uint32_t contentBg = packColor(&ds->ctx->display, 255, 255, 255); // Get display text const char *caption = getPropValue(ctrl, "Caption"); const char *text = getPropValue(ctrl, "Text"); const char *label = caption ? caption : (text ? text : ctrl->name); if (strcasecmp(ctrl->typeName, "CommandButton") == 0) { // Raised button wgtCanvasSetPenColor(cv, face); wgtCanvasFillRect(cv, x, y, w, h); wgtCanvasSetPenColor(cv, hilight); wgtCanvasDrawLine(cv, x, y, x + w - 1, y); wgtCanvasDrawLine(cv, x, y, x, y + h - 1); wgtCanvasSetPenColor(cv, dkShadow); wgtCanvasDrawLine(cv, x + w - 1, y, x + w - 1, y + h - 1); wgtCanvasDrawLine(cv, x, y + h - 1, x + w - 1, y + h - 1); wgtCanvasSetPenColor(cv, shadow); wgtCanvasDrawLine(cv, x + w - 2, y + 1, x + w - 2, y + h - 2); wgtCanvasDrawLine(cv, x + 1, y + h - 2, x + w - 2, y + h - 2); // Center text int32_t tw = (int32_t)strlen(label) * 8; wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + (w - tw) / 2, y + (h - 14) / 2, label); } else if (strcasecmp(ctrl->typeName, "Label") == 0) { // Just text, no border wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x, y + (h - 14) / 2, label); } else if (strcasecmp(ctrl->typeName, "TextBox") == 0) { // Sunken text field wgtCanvasSetPenColor(cv, contentBg); wgtCanvasFillRect(cv, x, y, w, h); wgtCanvasSetPenColor(cv, shadow); wgtCanvasDrawLine(cv, x, y, x + w - 1, y); wgtCanvasDrawLine(cv, x, y, x, y + h - 1); wgtCanvasSetPenColor(cv, hilight); wgtCanvasDrawLine(cv, x + w - 1, y, x + w - 1, y + h - 1); wgtCanvasDrawLine(cv, x, y + h - 1, x + w - 1, y + h - 1); wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + 2, y + (h - 14) / 2, text ? text : ""); } else if (strcasecmp(ctrl->typeName, "CheckBox") == 0) { // Check box + text wgtCanvasSetPenColor(cv, contentBg); wgtCanvasFillRect(cv, x, y + (h - 12) / 2, 12, 12); wgtCanvasSetPenColor(cv, shadow); wgtCanvasDrawRect(cv, x, y + (h - 12) / 2, 12, 12); wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + 16, y + (h - 14) / 2, label); } else if (strcasecmp(ctrl->typeName, "OptionButton") == 0) { // Radio circle + text wgtCanvasSetPenColor(cv, shadow); wgtCanvasFillCircle(cv, x + 6, y + h / 2, 6); wgtCanvasSetPenColor(cv, contentBg); wgtCanvasFillCircle(cv, x + 6, y + h / 2, 5); wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + 16, y + (h - 14) / 2, label); } else if (strcasecmp(ctrl->typeName, "Frame") == 0) { // Titled border wgtCanvasSetPenColor(cv, shadow); wgtCanvasDrawRect(cv, x, y + 6, w, h - 6); wgtCanvasSetPenColor(cv, hilight); wgtCanvasDrawRect(cv, x + 1, y + 7, w - 2, h - 8); wgtCanvasSetPenColor(cv, face); wgtCanvasFillRect(cv, x + 8, y, (int32_t)strlen(label) * 8 + 4, 14); wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + 10, y, label); } else if (strcasecmp(ctrl->typeName, "Timer") == 0) { // Design-time icon (invisible at runtime) wgtCanvasSetPenColor(cv, shadow); wgtCanvasDrawRect(cv, x, y, w, h); wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + 2, y + 2, "TMR"); } else { // Generic: sunken rect with type name wgtCanvasSetPenColor(cv, contentBg); wgtCanvasFillRect(cv, x, y, w, h); wgtCanvasSetPenColor(cv, shadow); wgtCanvasDrawRect(cv, x, y, w, h); wgtCanvasSetPenColor(cv, black); wgtCanvasDrawText(cv, x + 2, y + (h - 14) / 2, label); } // Draw grab handles if selected if (selected) { drawHandles(ds, ctrl); } } // ============================================================ // drawGrid // ============================================================ static void drawGrid(DsgnStateT *ds) { uint32_t dotColor = packColor(&ds->ctx->display, 0, 0, 0); int32_t fw = ds->form->width; int32_t fh = ds->form->height; for (int32_t y = 0; y < fh; y += DSGN_GRID_SIZE) { for (int32_t x = 0; x < fw; x += DSGN_GRID_SIZE) { wgtCanvasSetPixel(ds->canvas, x, y, dotColor); } } } // ============================================================ // drawHandles // ============================================================ static void drawHandles(DsgnStateT *ds, const DsgnControlT *ctrl) { int32_t x = ctrl->left; int32_t y = ctrl->top; int32_t w = ctrl->width; int32_t h = ctrl->height; int32_t hs = DSGN_HANDLE_SIZE; uint32_t black = packColor(&ds->ctx->display, 0, 0, 0); wgtCanvasSetPenColor(ds->canvas, black); // 8 handles: NW, N, NE, E, SE, S, SW, W int32_t hx[HANDLE_COUNT] = { x - hs/2, x + w/2 - hs/2, x + w - hs/2, x + w - hs/2, x + w - hs/2, x + w/2 - hs/2, x - hs/2, x - hs/2 }; int32_t hy[HANDLE_COUNT] = { y - hs/2, y - hs/2, y - hs/2, y + h/2 - hs/2, y + h - hs/2, y + h - hs/2, y + h - hs/2, y + h/2 - hs/2 }; for (int32_t i = 0; i < HANDLE_COUNT; i++) { wgtCanvasFillRect(ds->canvas, hx[i], hy[i], hs, hs); } } // ============================================================ // 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) { // Iterate in reverse order (last = topmost) 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 (x >= ctrl->left && x < ctrl->left + ctrl->width && y >= ctrl->top && y < ctrl->top + ctrl->height) { return i; } } return -1; } // ============================================================ // hitTestHandles // ============================================================ static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y) { int32_t cx = ctrl->left; int32_t cy = ctrl->top; int32_t cw = ctrl->width; int32_t ch = ctrl->height; int32_t hs = DSGN_HANDLE_SIZE; int32_t hx[HANDLE_COUNT] = { 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[HANDLE_COUNT] = { 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 }; 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; } // ============================================================ // setPropValue // ============================================================ static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value) { // Update existing 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; } } // Add new 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++; } } // ============================================================ // snapToGrid // ============================================================ static int32_t snapToGrid(int32_t val, int32_t gridSize) { return ((val + gridSize / 2) / gridSize) * gridSize; }