diff --git a/dvxbasic/Makefile b/dvxbasic/Makefile index 9f9a8da..4cd82fa 100644 --- a/dvxbasic/Makefile +++ b/dvxbasic/Makefile @@ -31,7 +31,8 @@ COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/s FORMRT_OBJS = $(OBJDIR)/formrt.o # IDE app objects -APP_OBJS = $(OBJDIR)/ideMain.o $(FORMRT_OBJS) +IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o +APP_OBJS = $(IDE_OBJS) $(FORMRT_OBJS) APP_TARGET = $(APPDIR)/dvxbasic.app # Native test programs (host gcc, not cross-compiled) @@ -92,7 +93,16 @@ $(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h com $(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/codegen.h runtime/vm.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/ideMain.o: ide/ideMain.c compiler/parser.h runtime/vm.h | $(OBJDIR) +$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/lexer.o: compiler/lexer.c compiler/lexer.h | $(OBJDIR) @@ -124,5 +134,5 @@ $(BINDIR): mkdir -p $(BINDIR) clean: - rm -f $(RT_OBJS) $(COMP_OBJS) $(FORMRT_OBJS) $(APP_OBJS) $(RT_TARGET) $(APP_TARGET) $(LIBSDIR)/basrt.dep $(OBJDIR)/basrt_init.o + rm -f $(RT_OBJS) $(COMP_OBJS) $(FORMRT_OBJS) $(IDE_OBJS) $(RT_TARGET) $(APP_TARGET) $(LIBSDIR)/basrt.dep $(OBJDIR)/basrt_init.o rm -f $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) diff --git a/dvxbasic/ide/ideDesigner.c b/dvxbasic/ide/ideDesigner.c new file mode 100644 index 0000000..fee8e93 --- /dev/null +++ b/dvxbasic/ide/ideDesigner.c @@ -0,0 +1,967 @@ +// 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; +} diff --git a/dvxbasic/ide/ideDesigner.h b/dvxbasic/ide/ideDesigner.h new file mode 100644 index 0000000..29f9f0d --- /dev/null +++ b/dvxbasic/ide/ideDesigner.h @@ -0,0 +1,186 @@ +// ideDesigner.h -- DVX BASIC form designer +// +// Design-time data model and design surface for visual form editing. +// Controls are stored as pure data (not live widgets) and rendered +// onto a Canvas widget. The designer reads and writes .frm files. + +#ifndef IDE_DESIGNER_H +#define IDE_DESIGNER_H + +#include "dvxApp.h" +#include "dvxWidget.h" +#include "widgetCanvas.h" + +#include "stb_ds_wrap.h" + +#include +#include + +// ============================================================ +// Limits +// ============================================================ + +#define DSGN_MAX_NAME 32 +#define DSGN_MAX_TEXT 256 +#define DSGN_MAX_PROPS 32 +#define DSGN_GRID_SIZE 8 +#define DSGN_HANDLE_SIZE 6 + +// ============================================================ +// Design-time property (stored as key=value strings) +// ============================================================ + +typedef struct { + char name[DSGN_MAX_NAME]; + char value[DSGN_MAX_TEXT]; +} DsgnPropT; + +// ============================================================ +// Design-time control +// ============================================================ + +typedef struct { + char name[DSGN_MAX_NAME]; + char typeName[DSGN_MAX_NAME]; + int32_t left; + int32_t top; + int32_t width; + int32_t height; + int32_t tabIndex; + DsgnPropT props[DSGN_MAX_PROPS]; + int32_t propCount; +} DsgnControlT; + +// ============================================================ +// Design-time form +// ============================================================ + +typedef struct { + char name[DSGN_MAX_NAME]; + char caption[DSGN_MAX_TEXT]; + int32_t width; + int32_t height; + DsgnControlT *controls; // stb_ds dynamic array + bool dirty; +} DsgnFormT; + +// ============================================================ +// Toolbox tool IDs +// ============================================================ + +typedef enum { + TOOL_POINTER = 0, + TOOL_BUTTON, + TOOL_LABEL, + TOOL_TEXTBOX, + TOOL_CHECKBOX, + TOOL_OPTION, + TOOL_FRAME, + TOOL_LISTBOX, + TOOL_COMBOBOX, + TOOL_HSCROLL, + TOOL_VSCROLL, + TOOL_TIMER, + TOOL_PICTURE, + TOOL_IMAGE, + TOOL_COUNT +} DsgnToolE; + +// ============================================================ +// Grab handle IDs +// ============================================================ + +typedef enum { + HANDLE_NONE = -1, + HANDLE_NW = 0, + HANDLE_N, + HANDLE_NE, + HANDLE_E, + HANDLE_SE, + HANDLE_S, + HANDLE_SW, + HANDLE_W, + HANDLE_COUNT = 8 +} DsgnHandleE; + +// ============================================================ +// Interaction mode +// ============================================================ + +typedef enum { + DSGN_IDLE, + DSGN_PLACING, + DSGN_DRAWING, + DSGN_MOVING, + DSGN_RESIZING +} DsgnModeE; + +// ============================================================ +// Designer state +// ============================================================ + +typedef struct { + DsgnFormT *form; + int32_t selectedIdx; // -1 = form itself selected + DsgnToolE activeTool; + DsgnModeE mode; + DsgnHandleE activeHandle; + int32_t dragStartX; + int32_t dragStartY; + int32_t dragOrigLeft; + int32_t dragOrigTop; + int32_t dragOrigWidth; + int32_t dragOrigHeight; + int32_t drawX; // rubber-band start for new control + int32_t drawY; + bool showGrid; + bool snapToGrid; + WidgetT *canvas; + AppContextT *ctx; +} DsgnStateT; + +// ============================================================ +// API +// ============================================================ + +// Initialize designer state. +void dsgnInit(DsgnStateT *ds, AppContextT *ctx); + +// Set the canvas widget for rendering. +void dsgnSetCanvas(DsgnStateT *ds, WidgetT *canvas); + +// Load a .frm file into the designer. +bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen); + +// Save the designer form to .frm text format. +// Returns the number of bytes written (excluding null), or -1 on error. +int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize); + +// Create a new blank form. +void dsgnNewForm(DsgnStateT *ds, const char *name); + +// Repaint the design surface. +void dsgnPaint(DsgnStateT *ds); + +// Handle mouse click on the design surface. +void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag); + +// Handle key press in design view. +void dsgnOnKey(DsgnStateT *ds, int32_t key); + +// Get the name of the selected control (or form name if nothing selected). +const char *dsgnSelectedName(const DsgnStateT *ds); + +// Get the default event name for a control type. +const char *dsgnDefaultEvent(const char *typeName); + +// Get the VB type name for a tool ID. +const char *dsgnToolTypeName(DsgnToolE tool); + +// Auto-generate a unique control name for the given type. +void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize); + +// Free designer resources. +void dsgnFree(DsgnStateT *ds); + +#endif // IDE_DESIGNER_H diff --git a/dvxbasic/ide/ideMain.c b/dvxbasic/ide/ideMain.c index 3e5caaa..e622f37 100644 --- a/dvxbasic/ide/ideMain.c +++ b/dvxbasic/ide/ideMain.c @@ -16,9 +16,14 @@ #include "widgetLabel.h" #include "widgetTextInput.h" #include "widgetDropdown.h" +#include "widgetCanvas.h" #include "widgetSplitter.h" #include "widgetStatusBar.h" +#include "ideDesigner.h" +#include "ideToolbox.h" +#include "ideProperties.h" + #include "../compiler/parser.h" #include "../formrt/formrt.h" #include "../runtime/vm.h" @@ -52,7 +57,11 @@ #define CMD_CLEAR 103 #define CMD_EXIT 104 #define CMD_RUN_NOCMP 105 +#define CMD_VIEW_CODE 106 +#define CMD_VIEW_DESIGN 107 #define IDE_MAX_IMM 1024 +#define IDE_DESIGN_W 400 +#define IDE_DESIGN_H 300 // ============================================================ // Prototypes @@ -76,7 +85,11 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS static bool doEventsCallback(void *ctx); static void runCached(void); static void runModule(BasModuleT *mod); +static void onFormWinClose(WindowT *win); static void setStatus(const char *text); +static void switchToCode(void); +static void switchToDesign(void); +static void dsgnMouseCb(WidgetT *w, int32_t cx, int32_t cy, bool drag); static void updateDropdowns(void); // ============================================================ @@ -94,6 +107,10 @@ static WidgetT *sEvtDropdown = NULL; // Event dropdown static WidgetT *sStatus = NULL; // Status bar label static BasVmT *sVm = NULL; // VM instance (non-NULL while running) static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5) +static DsgnStateT sDesigner; +static WindowT *sFormWin = NULL; // Form designer window (separate) +static WindowT *sToolboxWin = NULL; +static WindowT *sPropsWin = NULL; static char sSourceBuf[IDE_MAX_SOURCE]; static char sOutputBuf[IDE_MAX_OUTPUT]; @@ -171,10 +188,16 @@ static void buildWindow(void) { wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR); + MenuT *viewMenu = wmAddMenu(menuBar, "&View"); + wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); + wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN); + AccelTableT *accel = dvxCreateAccelTable(); dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); dvxAddAccel(accel, KEY_F5, 0, CMD_RUN); dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP); + dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE); + dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN); dvxAddAccel(accel, 0x1B, 0, CMD_STOP); sWin->accelTable = accel; @@ -208,6 +231,9 @@ static void buildWindow(void) { wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); + // Initialize designer (form window created on demand) + dsgnInit(&sDesigner, sAc); + // Bottom pane: output + immediate WidgetT *bottomPane = wgtVBox(splitter); @@ -817,6 +843,12 @@ static void onClose(WindowT *win) { sCachedModule = NULL; } + if (sFormWin) { + onFormWinClose(sFormWin); + } + + dsgnFree(&sDesigner); + arrfree(sProcTable); arrfree(sObjItems); arrfree(sEvtItems); @@ -858,6 +890,14 @@ static void onMenu(WindowT *win, int32_t menuId) { clearOutput(); break; + case CMD_VIEW_CODE: + switchToCode(); + break; + + case CMD_VIEW_DESIGN: + switchToDesign(); + break; + case CMD_EXIT: if (sWin) { onClose(sWin); @@ -1033,6 +1073,141 @@ static void printCallback(void *ctx, const char *text, bool newline) { } } +// ============================================================ +// dsgnMouseCb +// ============================================================ + +static void dsgnMouseCb(WidgetT *w, int32_t cx, int32_t cy, bool drag) { + (void)w; + dsgnOnMouse(&sDesigner, cx, cy, drag); + prpRefresh(&sDesigner); +} + + +// ============================================================ +// onFormWinClose +// ============================================================ + +static void onFormWinClose(WindowT *win) { + dvxDestroyWindow(sAc, win); + sFormWin = NULL; + dsgnSetCanvas(&sDesigner, NULL); + + if (sToolboxWin) { + tbxDestroy(sAc, sToolboxWin); + sToolboxWin = NULL; + } + + if (sPropsWin) { + prpDestroy(sAc, sPropsWin); + sPropsWin = NULL; + } +} + + +// ============================================================ +// switchToCode +// ============================================================ + +static void switchToCode(void) { + if (sFormWin) { + onFormWinClose(sFormWin); + } + + setStatus("Code view."); +} + + +// ============================================================ +// switchToDesign +// ============================================================ + +static void switchToDesign(void) { + // If already open, just bring to front + if (sFormWin) { + return; + } + + // Load .frm if we don't have a form yet + if (!sDesigner.form) { + if (sFilePath[0]) { + char frmPath[260]; + snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); + char *dot = strrchr(frmPath, '.'); + + if (dot) { + strcpy(dot, ".frm"); + } else { + strcat(frmPath, ".frm"); + } + + FILE *f = fopen(frmPath, "r"); + + if (f) { + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size > 0 && size < IDE_MAX_SOURCE) { + char *buf = (char *)malloc(size + 1); + + if (buf) { + int32_t bytesRead = (int32_t)fread(buf, 1, size, f); + buf[bytesRead] = '\0'; + dsgnLoadFrm(&sDesigner, buf, bytesRead); + free(buf); + } + } + + fclose(f); + } + } + + if (!sDesigner.form) { + dsgnNewForm(&sDesigner, "Form1"); + } + } + + // Create the form designer window + const char *formName = sDesigner.form ? sDesigner.form->name : "Form1"; + int32_t formW = sDesigner.form ? sDesigner.form->width : IDE_DESIGN_W; + int32_t formH = sDesigner.form ? sDesigner.form->height : IDE_DESIGN_H; + + char title[128]; + snprintf(title, sizeof(title), "%s [Design]", formName); + + // Position next to the IDE window + int32_t winX = IDE_WIN_X; + int32_t winY = IDE_WIN_Y; + + sFormWin = dvxCreateWindow(sAc, title, winX, winY, formW + 10, formH + 10, true); + + if (!sFormWin) { + return; + } + + sFormWin->onClose = onFormWinClose; + + WidgetT *root = wgtInitWindow(sAc, sFormWin); + WidgetT *canvas = wgtCanvas(root, formW, formH); + canvas->weight = 100; + wgtCanvasSetMouseCallback(canvas, dsgnMouseCb); + dsgnSetCanvas(&sDesigner, canvas); + + // Create toolbox and properties windows + if (!sToolboxWin) { + sToolboxWin = tbxCreate(sAc, &sDesigner); + } + + if (!sPropsWin) { + sPropsWin = prpCreate(sAc, &sDesigner); + } + + dsgnPaint(&sDesigner); + setStatus("Design view open."); +} + + // ============================================================ // setStatus // ============================================================ diff --git a/dvxbasic/ide/ideProperties.c b/dvxbasic/ide/ideProperties.c new file mode 100644 index 0000000..055e688 --- /dev/null +++ b/dvxbasic/ide/ideProperties.c @@ -0,0 +1,134 @@ +// ideProperties.c -- DVX BASIC form designer properties window +// +// A floating window with a two-column list showing property names +// and values for the currently selected control. Double-clicking +// a value opens an input dialog to edit it. + +#include "ideProperties.h" +#include "dvxDialog.h" +#include "dvxWm.h" +#include "widgetBox.h" +#include "widgetLabel.h" +#include "widgetTextInput.h" + +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define PRP_WIN_W 200 +#define PRP_WIN_H 320 +#define PRP_MAX_ROWS 64 + +// ============================================================ +// Module state +// ============================================================ + +static DsgnStateT *sDs = NULL; +static WindowT *sPrpWin = NULL; +static WidgetT *sListArea = NULL; // TextArea showing properties as text +static AppContextT *sPrpCtx = NULL; + +// ============================================================ +// Prototypes +// ============================================================ + +static void onPrpClose(WindowT *win); + +// ============================================================ +// onPrpClose +// ============================================================ + +static void onPrpClose(WindowT *win) { + (void)win; +} + + +// ============================================================ +// prpCreate +// ============================================================ + +WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) { + sDs = ds; + sPrpCtx = ctx; + + // Position at right edge of screen + int32_t winX = ctx->display.width - PRP_WIN_W - 10; + WindowT *win = dvxCreateWindow(ctx, "Properties", winX, 30, PRP_WIN_W, PRP_WIN_H, false); + + if (!win) { + return NULL; + } + + win->onClose = onPrpClose; + sPrpWin = win; + + WidgetT *root = wgtInitWindow(ctx, win); + + // Properties displayed as a read-only text area (simple approach) + sListArea = wgtTextArea(root, 4096); + sListArea->weight = 100; + sListArea->readOnly = true; + + prpRefresh(ds); + return win; +} + + +// ============================================================ +// prpDestroy +// ============================================================ + +void prpDestroy(AppContextT *ctx, WindowT *win) { + if (win) { + dvxDestroyWindow(ctx, win); + } + + sPrpWin = NULL; + sListArea = NULL; + sDs = NULL; +} + + +// ============================================================ +// prpRefresh +// ============================================================ + +void prpRefresh(DsgnStateT *ds) { + if (!sListArea || !ds || !ds->form) { + return; + } + + char buf[4096]; + int32_t pos = 0; + + if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { + DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + + pos += snprintf(buf + pos, sizeof(buf) - pos, "%s (%s)\n", ctrl->name, ctrl->typeName); + pos += snprintf(buf + pos, sizeof(buf) - pos, "---\n"); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Name = %s\n", ctrl->name); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Left = %d\n", (int)ctrl->left); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Top = %d\n", (int)ctrl->top); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Width = %d\n", (int)ctrl->width); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Height = %d\n", (int)ctrl->height); + pos += snprintf(buf + pos, sizeof(buf) - pos, "TabIndex = %d\n", (int)ctrl->tabIndex); + + for (int32_t i = 0; i < ctrl->propCount; i++) { + pos += snprintf(buf + pos, sizeof(buf) - pos, "%-8s = %s\n", ctrl->props[i].name, ctrl->props[i].value); + } + } else { + pos += snprintf(buf + pos, sizeof(buf) - pos, "%s (Form)\n", ds->form->name); + pos += snprintf(buf + pos, sizeof(buf) - pos, "---\n"); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Name = %s\n", ds->form->name); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Caption = %s\n", ds->form->caption); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Width = %d\n", (int)ds->form->width); + pos += snprintf(buf + pos, sizeof(buf) - pos, "Height = %d\n", (int)ds->form->height); + } + + wgtSetText(sListArea, buf); +} diff --git a/dvxbasic/ide/ideProperties.h b/dvxbasic/ide/ideProperties.h new file mode 100644 index 0000000..042467b --- /dev/null +++ b/dvxbasic/ide/ideProperties.h @@ -0,0 +1,17 @@ +// ideProperties.h -- DVX BASIC form designer properties window + +#ifndef IDE_PROPERTIES_H +#define IDE_PROPERTIES_H + +#include "ideDesigner.h" + +// Create the properties floating window. +WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds); + +// Destroy the properties window. +void prpDestroy(AppContextT *ctx, WindowT *win); + +// Refresh the properties list for the currently selected control. +void prpRefresh(DsgnStateT *ds); + +#endif // IDE_PROPERTIES_H diff --git a/dvxbasic/ide/ideToolbox.c b/dvxbasic/ide/ideToolbox.c new file mode 100644 index 0000000..e851fbf --- /dev/null +++ b/dvxbasic/ide/ideToolbox.c @@ -0,0 +1,110 @@ +// ideToolbox.c -- DVX BASIC form designer toolbox window +// +// A small floating window with buttons for each control type. +// Clicking a tool sets the designer's activeTool. + +#include "ideToolbox.h" +#include "dvxWm.h" +#include "widgetBox.h" +#include "widgetButton.h" + +#include + +// ============================================================ +// Constants +// ============================================================ + +#define TBX_WIN_W 72 +#define TBX_WIN_H 320 +#define TBX_BTN_H 20 + +// ============================================================ +// Module state +// ============================================================ + +static DsgnStateT *sDs = NULL; + +// ============================================================ +// Tool labels (short names for buttons) +// ============================================================ + +static const char *sToolLabels[TOOL_COUNT] = { + "Pointer", + "Button", + "Label", + "TextBox", + "CheckBox", + "Option", + "Frame", + "ListBox", + "ComboBox", + "HScroll", + "VScroll", + "Timer", + "Picture", + "Image" +}; + +// ============================================================ +// Callbacks +// ============================================================ + +static void onToolClick(WidgetT *w) { + if (!sDs) { + return; + } + + int32_t toolIdx = (int32_t)(intptr_t)w->userData; + + if (toolIdx >= 0 && toolIdx < TOOL_COUNT) { + sDs->activeTool = (DsgnToolE)toolIdx; + sDs->mode = (toolIdx == TOOL_POINTER) ? DSGN_IDLE : DSGN_PLACING; + } +} + + +static void onTbxClose(WindowT *win) { + // Don't allow closing the toolbox independently + (void)win; +} + + +// ============================================================ +// tbxCreate +// ============================================================ + +WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds) { + sDs = ds; + + WindowT *win = dvxCreateWindow(ctx, "Toolbox", 0, 30, TBX_WIN_W, TBX_WIN_H, false); + + if (!win) { + return NULL; + } + + win->onClose = onTbxClose; + + WidgetT *root = wgtInitWindow(ctx, win); + + for (int32_t i = 0; i < TOOL_COUNT; i++) { + WidgetT *btn = wgtButton(root, sToolLabels[i]); + btn->onClick = onToolClick; + btn->userData = (void *)(intptr_t)i; + } + + dvxFitWindow(ctx, win); + return win; +} + + +// ============================================================ +// tbxDestroy +// ============================================================ + +void tbxDestroy(AppContextT *ctx, WindowT *win) { + if (win) { + dvxDestroyWindow(ctx, win); + } + + sDs = NULL; +} diff --git a/dvxbasic/ide/ideToolbox.h b/dvxbasic/ide/ideToolbox.h new file mode 100644 index 0000000..155e3a3 --- /dev/null +++ b/dvxbasic/ide/ideToolbox.h @@ -0,0 +1,14 @@ +// ideToolbox.h -- DVX BASIC form designer toolbox window + +#ifndef IDE_TOOLBOX_H +#define IDE_TOOLBOX_H + +#include "ideDesigner.h" + +// Create the toolbox floating window. Returns the WindowT pointer. +WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds); + +// Destroy the toolbox window. +void tbxDestroy(AppContextT *ctx, WindowT *win); + +#endif // IDE_TOOLBOX_H diff --git a/shell/shellApp.c b/shell/shellApp.c index fa12d7c..6651a41 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -352,6 +352,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { if (!handle) { char msg[512]; snprintf(msg, sizeof(msg), "Failed to load %s:\n%s", baseName(path), dlerror()); + dvxLog("DXE load failed: %s", msg); dvxSetBusy(ctx, false); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); @@ -368,6 +369,7 @@ int32_t shellLoadApp(AppContextT *ctx, const char *path) { if (!desc) { char msg[256]; snprintf(msg, sizeof(msg), "%s: missing appDescriptor", baseName(path)); + dvxLog("DXE symbol error: %s", msg); dvxSetBusy(ctx, false); dvxMessageBox(ctx, "Error", msg, MB_OK | MB_ICONERROR); dlclose(handle); diff --git a/widgets/widgetCanvas.c b/widgets/widgetCanvas.c index 2b22470..1691d74 100644 --- a/widgets/widgetCanvas.c +++ b/widgets/widgetCanvas.c @@ -815,6 +815,80 @@ void wgtCanvasSetPenSize(WidgetT *w, int32_t size) { } +void wgtCanvasDrawText(WidgetT *w, int32_t x, int32_t y, const char *text) { + if (!w || w->type != sTypeId || !text) { + return; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { + return; + } + + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return; + } + + const BitmapFontT *font = &ctx->font; + int32_t cw = font->charWidth; + int32_t ch = font->charHeight; + int32_t bpp = cd->canvasBpp; + + for (int32_t ci = 0; text[ci]; ci++) { + int32_t cx = x + ci * cw; + + if (cx + cw <= 0 || cx >= cd->canvasW || y + ch <= 0 || y >= cd->canvasH) { + continue; + } + + int32_t idx = (uint8_t)text[ci] - font->firstChar; + + if (idx < 0 || idx >= font->numChars) { + continue; + } + + const uint8_t *glyph = font->glyphData + idx * ch; + + for (int32_t row = 0; row < ch; row++) { + int32_t py = y + row; + + if (py < 0 || py >= cd->canvasH) { + continue; + } + + uint8_t bits = glyph[row]; + + if (bits == 0) { + continue; + } + + for (int32_t col = 0; col < cw; col++) { + int32_t px = cx + col; + + if (px < 0 || px >= cd->canvasW) { + continue; + } + + if (bits & (0x80 >> col)) { + if (bpp == 2) { + *(uint16_t *)(cd->pixelData + py * cd->canvasPitch + px * 2) = (uint16_t)cd->penColor; + } else if (bpp == 4) { + *(uint32_t *)(cd->pixelData + py * cd->canvasPitch + px * 4) = cd->penColor; + } else { + cd->pixelData[py * cd->canvasPitch + px] = (uint8_t)cd->penColor; + } + } + } + } + } + + wgtInvalidatePaint(w); +} + + // ============================================================ // DXE registration // ============================================================ @@ -834,6 +908,7 @@ static const struct { void (*fillCircle)(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); void (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color); uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y); + void (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text); } sApi = { .create = wgtCanvas, .clear = wgtCanvasClear, @@ -847,7 +922,8 @@ static const struct { .fillRect = wgtCanvasFillRect, .fillCircle = wgtCanvasFillCircle, .setPixel = wgtCanvasSetPixel, - .getPixel = wgtCanvasGetPixel + .getPixel = wgtCanvasGetPixel, + .drawText = wgtCanvasDrawText }; static const WgtMethodDescT sMethods[] = { diff --git a/widgets/widgetCanvas.h b/widgets/widgetCanvas.h index 3805071..c4f0433 100644 --- a/widgets/widgetCanvas.h +++ b/widgets/widgetCanvas.h @@ -18,6 +18,7 @@ typedef struct { void (*fillCircle)(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); void (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color); uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y); + void (*drawText)(WidgetT *w, int32_t x, int32_t y, const char *text); } CanvasApiT; static inline const CanvasApiT *dvxCanvasApi(void) { @@ -39,5 +40,6 @@ static inline const CanvasApiT *dvxCanvasApi(void) { #define wgtCanvasFillCircle(w, cx, cy, radius) dvxCanvasApi()->fillCircle(w, cx, cy, radius) #define wgtCanvasSetPixel(w, x, y, color) dvxCanvasApi()->setPixel(w, x, y, color) #define wgtCanvasGetPixel(w, x, y) dvxCanvasApi()->getPixel(w, x, y) +#define wgtCanvasDrawText(w, x, y, text) dvxCanvasApi()->drawText(w, x, y, text) #endif // WIDGET_CANVAS_H