DVX_GUI/dvxbasic/ide/ideDesigner.c

967 lines
32 KiB
C

// 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 <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// 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;
}