More IDE work.

This commit is contained in:
Scott Duensing 2026-03-28 21:14:38 -05:00
parent 7d74d76985
commit 59bc2b5ed3
7 changed files with 722 additions and 471 deletions

View file

@ -3259,10 +3259,21 @@ static void updateCursorShape(AppContextT *ctx) {
newCursor = CURSOR_RESIZE_V;
}
} else if (hitIdx >= 0 && hitPart == HIT_CONTENT) {
// Hovering over content area -- check for ListView column border
WindowT *win = ctx->stack.windows[hitIdx];
if (win->widgetRoot) {
// Window-level cursor query (e.g. form designer handles)
if (win->onCursorQuery) {
int32_t cx = mx - win->x - win->contentX;
int32_t cy = my - win->y - win->contentY;
int32_t shape = win->onCursorQuery(win, cx, cy);
if (shape > 0) {
newCursor = shape;
}
}
// Widget-level cursor query (e.g. ListView column border)
if (newCursor == CURSOR_ARROW && win->widgetRoot) {
int32_t cx = mx - win->x - win->contentX;
int32_t cy = my - win->y - win->contentY;
int32_t scrollX = win->hScroll ? win->hScroll->value : 0;

View file

@ -555,6 +555,7 @@ typedef struct WindowT {
void (*onClose)(struct WindowT *win);
void (*onMenu)(struct WindowT *win, int32_t menuId);
void (*onScroll)(struct WindowT *win, ScrollbarOrientE orient, int32_t value);
int32_t (*onCursorQuery)(struct WindowT *win, int32_t x, int32_t y); // return CURSOR_* or 0 for default
} WindowT;
// ============================================================

View file

@ -1,10 +1,11 @@
// 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.
// WYSIWYG design surface using real DVX widgets positioned
// identically to how the form runtime lays them out. Selection
// handles and grid dots are drawn in the window's onPaint callback.
#include "ideDesigner.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "stb_ds_wrap.h"
@ -24,6 +25,7 @@
#define DEFAULT_CTRL_W 100
#define DEFAULT_CTRL_H 30
#define MIN_CTRL_SIZE 8
#define DEFAULT_CREATE_ARG 256
// ============================================================
// Tool type name table
@ -75,14 +77,68 @@ static const DefaultEventT sDefaultEvents[] = {
// 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 WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent);
static const char *getPropValue(const DsgnControlT *ctrl, const char *name);
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
static int32_t snapToGrid(int32_t val, int32_t gridSize);
static const char *resolveTypeName(const char *typeName);
static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
static void rebuildWidgets(DsgnStateT *ds);
static void syncWidgetGeom(DsgnControlT *ctrl);
// ============================================================
// createDesignWidget
// ============================================================
//
// Create a real DVX widget for design-time display. Mirrors the
// logic in formrt.c createWidget().
static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) {
const char *wgtName = resolveTypeName(vbTypeName);
if (!wgtName) {
return NULL;
}
const void *api = wgtGetApi(wgtName);
if (!api) {
return NULL;
}
typedef WidgetT *(*CreateParentFnT)(WidgetT *);
typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
if (strcasecmp(wgtName, "button") == 0 ||
strcasecmp(wgtName, "checkbox") == 0 ||
strcasecmp(wgtName, "label") == 0) {
CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
return fn(parent, "");
}
if (strcasecmp(wgtName, "textinput") == 0 ||
strcasecmp(wgtName, "combobox") == 0) {
CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
return fn(parent, DEFAULT_CREATE_ARG);
}
if (strcasecmp(wgtName, "timer") == 0) {
CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
return fn(parent, 1000, false);
}
if (strcasecmp(wgtName, "listbox") == 0) {
CreateParentFnT fn = *(CreateParentFnT *)api;
return fn(parent);
}
// Fallback: try simple create(parent)
CreateParentFnT fn = *(CreateParentFnT *)api;
return fn(parent);
}
// ============================================================
@ -90,7 +146,6 @@ static void setPropValue(DsgnControlT *ctrl, const char *name, const char *v
// ============================================================
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"; }
@ -117,6 +172,50 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t
}
// ============================================================
// dsgnCreateWidgets
// ============================================================
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
if (!ds->form || !contentBox) {
return;
}
ds->form->contentBox = contentBox;
int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = &ds->form->controls[i];
if (ctrl->widget) {
continue; // already created
}
WidgetT *w = createDesignWidget(ctrl->typeName, contentBox);
if (!w) {
continue;
}
ctrl->widget = w;
wgtSetName(w, ctrl->name);
// Set Caption/Text
const char *caption = getPropValue(ctrl, "Caption");
const char *text = getPropValue(ctrl, "Text");
if (caption) { wgtSetText(w, caption); }
if (text) { wgtSetText(w, text); }
// Set size hints for the layout engine
if (ctrl->height > 0) {
w->minH = wgtPixels(ctrl->height);
w->prefH = wgtPixels(ctrl->height);
}
}
}
// ============================================================
// dsgnDefaultEvent
// ============================================================
@ -155,8 +254,6 @@ void dsgnInit(DsgnStateT *ds, AppContextT *ctx) {
ds->activeTool = TOOL_POINTER;
ds->mode = DSGN_IDLE;
ds->activeHandle = HANDLE_NONE;
ds->showGrid = true;
ds->snapToGrid = true;
ds->ctx = ctx;
}
@ -191,7 +288,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
const char *end = source + sourceLen;
while (pos < end) {
// Extract one line
const char *lineStart = pos;
while (pos < end && *pos != '\n' && *pos != '\r') {
@ -212,7 +308,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
memcpy(line, lineStart, lineLen);
line[lineLen] = '\0';
// Trim leading whitespace
char *trimmed = line;
while (*trimmed == ' ' || *trimmed == '\t') {
@ -223,7 +318,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
continue;
}
// VERSION line -- skip
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
continue;
}
@ -271,7 +365,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
continue;
}
// End
if (strcasecmp(trimmed, "End") == 0) {
if (curCtrl) {
curCtrl = NULL;
@ -286,7 +379,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
char *eq = strchr(trimmed, '=');
if (eq && inForm) {
// Extract key
char key[DSGN_MAX_NAME];
char *kend = eq - 1;
@ -299,7 +391,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
memcpy(key, trimmed, klen);
key[klen] = '\0';
// Extract value (skip leading whitespace and quotes)
char *vstart = eq + 1;
while (*vstart == ' ' || *vstart == '\t') { vstart++; }
@ -318,13 +409,11 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
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); }
@ -340,27 +429,6 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
}
}
// 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;
@ -395,12 +463,11 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
return;
}
// Delete key -- remove selected control
// Delete key
if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) {
arrdel(ds->form->controls, ds->selectedIdx);
ds->selectedIdx = -1;
ds->form->dirty = true;
dsgnPaint(ds);
}
}
@ -410,69 +477,29 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) {
// ============================================================
void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
if (!ds->form || !ds->canvas) {
if (!ds->form) {
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) {
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;
case HANDLE_SE:
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;
}
@ -480,100 +507,57 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
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; }
}
syncWidgetGeom(ctrl);
ds->form->dirty = true;
dsgnPaint(ds);
} else if (ds->mode == DSGN_DRAWING) {
// Rubber-band for new control -- just repaint with preview
dsgnPaint(ds);
} else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
// Determine if we should swap with a neighbor based on drag direction
int32_t dy = y - ds->dragStartY;
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx];
// 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 (dy > 0 && ctrl->widget) {
// Dragging down -- swap with next control if past its midpoint
if (ds->selectedIdx < ctrlCount - 1) {
DsgnControlT *next = &ds->form->controls[ds->selectedIdx + 1];
if (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;
if (next->widget && y > next->widget->y + next->widget->h / 2) {
DsgnControlT tmp = ds->form->controls[ds->selectedIdx];
ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1];
ds->form->controls[ds->selectedIdx + 1] = tmp;
rebuildWidgets(ds);
ds->selectedIdx++;
ds->dragStartY = y;
ds->form->dirty = true;
dsgnPaint(ds);
}
}
} else if (dy < 0 && ctrl->widget) {
// Dragging up -- swap with previous control if past its midpoint
if (ds->selectedIdx > 0) {
DsgnControlT *prev = &ds->form->controls[ds->selectedIdx - 1];
if (prev->widget && y < prev->widget->y + prev->widget->h / 2) {
DsgnControlT tmp = ds->form->controls[ds->selectedIdx];
ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1];
ds->form->controls[ds->selectedIdx - 1] = tmp;
rebuildWidgets(ds);
ds->selectedIdx--;
ds->dragStartY = y;
ds->form->dirty = true;
}
}
}
}
return;
}
// Release from moving/resizing
if (ds->mode == DSGN_MOVING || ds->mode == DSGN_RESIZING) {
// Mouse click (not drag) -- end any ongoing operation
if (ds->mode == DSGN_REORDERING || 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
// Pointer tool: select, start resize or reorder
if (ds->activeTool == TOOL_POINTER) {
// Check grab handles of selected control first
if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) {
DsgnHandleE handle = hitTestHandles(&ds->form->controls[ds->selectedIdx], x, y);
@ -584,59 +568,132 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
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;
ds->dragOrigWidth = ctrl->widget ? ctrl->widget->w : ctrl->width;
ds->dragOrigHeight = ctrl->widget ? ctrl->widget->h : ctrl->height;
return;
}
}
// Hit test controls
// Hit test controls -- click to select, drag to reorder
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->mode = DSGN_REORDERING;
ds->dragStartY = y;
ds->dragOrigLeft = ctrl->left;
ds->dragOrigTop = ctrl->top;
} else {
ds->selectedIdx = -1;
}
dsgnPaint(ds);
return;
}
// Non-pointer tool: place a new control (appended to VBox)
const char *typeName = dsgnToolTypeName(ds->activeTool);
DsgnControlT ctrl;
memset(&ctrl, 0, sizeof(ctrl));
dsgnAutoName(ds, typeName, ctrl.name, DSGN_MAX_NAME);
snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName);
ctrl.width = DEFAULT_CTRL_W;
ctrl.height = DEFAULT_CTRL_H;
ctrl.tabIndex = ctrlCount;
setPropValue(&ctrl, "Caption", ctrl.name);
// Create the live widget
if (ds->form->contentBox) {
ctrl.widget = createDesignWidget(typeName, ds->form->contentBox);
if (ctrl.widget) {
wgtSetName(ctrl.widget, ctrl.name);
wgtSetText(ctrl.widget, ctrl.name);
ctrl.widget->prefW = wgtPixels(ctrl.width);
ctrl.widget->prefH = wgtPixels(ctrl.height);
}
}
arrput(ds->form->controls, ctrl);
ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1;
ds->activeTool = TOOL_POINTER;
ds->mode = DSGN_IDLE;
ds->form->dirty = true;
}
// ============================================================
// dsgnPaint
// ============================================================
//
// Draw grid dots and selection handles over the live widgets.
// Called from the form window's onPaint callback.
void dsgnPaint(DsgnStateT *ds) {
if (!ds->canvas || !ds->form || !ds->ctx) {
if (!ds->form || !ds->ctx) {
return;
}
// Form background
uint32_t formBg = packColor(&ds->ctx->display, 192, 192, 192);
wgtCanvasClear(ds->canvas, formBg);
// Grid dots and selection handles are drawn directly onto
// the window via the display backbuffer. The live widgets
// handle their own rendering.
// Grid dots
if (ds->showGrid) {
drawGrid(ds);
// Nothing to do here for now -- the onPaint hook in ideMain
// calls dsgnPaintOverlay with the display pointer.
}
// ============================================================
// dsgnPaintOverlay
// ============================================================
//
// Draw selection handles on the window's painted surface.
// Called after widgets have painted, using direct display drawing.
void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) {
(void)winX;
(void)winY;
if (!ds->form || !ds->form->controls || !ds->ctx || !ds->formWin || !ds->formWin->contentBuf) {
return;
}
// 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);
if (ds->selectedIdx < 0 || ds->selectedIdx >= count) {
return;
}
wgtInvalidatePaint(ds->canvas);
DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx];
if (!ctrl->widget || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
return;
}
// Draw into the window's content buffer (same coordinate space as widgets)
DisplayT cd = ds->ctx->display;
cd.backBuf = ds->formWin->contentBuf;
cd.width = ds->formWin->contentW;
cd.height = ds->formWin->contentH;
cd.pitch = ds->formWin->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = ds->formWin->contentW;
cd.clipH = ds->formWin->contentH;
const BlitOpsT *ops = &ds->ctx->blitOps;
uint32_t black = packColor(&cd, 0, 0, 0);
int32_t cx = ctrl->widget->x;
int32_t cy = ctrl->widget->y;
int32_t cw = ctrl->widget->w;
int32_t ch = ctrl->widget->h;
int32_t hs = DSGN_HANDLE_SIZE;
// 3 handles: E (right edge), S (bottom edge), SE (corner)
int32_t hx[HANDLE_COUNT] = { cx + cw - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2 };
int32_t hy[HANDLE_COUNT] = { cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2 };
for (int32_t i = 0; i < HANDLE_COUNT; i++) {
rectFill(&cd, ops, hx[i], hy[i], hs, hs, black);
}
}
@ -663,20 +720,17 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) {
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; }
@ -709,13 +763,6 @@ const char *dsgnSelectedName(const DsgnStateT *ds) {
}
// ============================================================
// dsgnSetCanvas
// ============================================================
void dsgnSetCanvas(DsgnStateT *ds, WidgetT *canvas) {
ds->canvas = canvas;
}
// ============================================================
@ -731,152 +778,6 @@ const char *dsgnToolTypeName(DsgnToolE tool) {
}
// ============================================================
// 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
// ============================================================
@ -897,13 +798,22 @@ static const char *getPropValue(const DsgnControlT *ctrl, const char *name) {
// ============================================================
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) {
if (!ctrl->widget) {
continue;
}
// Use the widget's actual laid-out position
int32_t wx = ctrl->widget->x;
int32_t wy = ctrl->widget->y;
int32_t ww = ctrl->widget->w;
int32_t wh = ctrl->widget->h;
if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) {
return i;
}
}
@ -917,14 +827,19 @@ static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) {
// ============================================================
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;
if (!ctrl->widget) {
return HANDLE_NONE;
}
int32_t cx = ctrl->widget->x;
int32_t cy = ctrl->widget->y;
int32_t cw = ctrl->widget->w;
int32_t ch = ctrl->widget->h;
int32_t hs = DSGN_HANDLE_SIZE;
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 };
// 3 handles: E (right edge), S (bottom edge), SE (corner)
int32_t hx[HANDLE_COUNT] = { cx + cw - hs/2, cx + cw/2 - hs/2, cx + cw - hs/2 };
int32_t hy[HANDLE_COUNT] = { cy + ch/2 - hs/2, cy + ch - hs/2, cy + ch - hs/2 };
for (int32_t i = 0; i < HANDLE_COUNT; i++) {
if (x >= hx[i] && x < hx[i] + hs && y >= hy[i] && y < hy[i] + hs) {
@ -936,12 +851,30 @@ static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y
}
// ============================================================
// resolveTypeName
// ============================================================
static const char *resolveTypeName(const char *typeName) {
const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) {
return wgtName;
}
if (wgtGetApi(typeName)) {
return typeName;
}
return NULL;
}
// ============================================================
// setPropValue
// ============================================================
static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value) {
// 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);
@ -949,7 +882,6 @@ static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value
}
}
// 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);
@ -958,10 +890,54 @@ static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value
}
// ============================================================
// snapToGrid
// ============================================================
static int32_t snapToGrid(int32_t val, int32_t gridSize) {
return ((val + gridSize / 2) / gridSize) * gridSize;
// ============================================================
// rebuildWidgets
// ============================================================
//
// Destroy all live widgets and recreate them in the current
// array order. This is the safest way to reorder since the
// widget tree child list matches the creation order.
static void rebuildWidgets(DsgnStateT *ds) {
WidgetT *parent = ds->form ? ds->form->contentBox : NULL;
if (!parent) {
return;
}
// Destroy all existing widget children
// (wgtDestroy not available, so just unlink and let the window own them)
parent->firstChild = NULL;
parent->lastChild = NULL;
// Clear widget pointers
int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) {
ds->form->controls[i].widget = NULL;
}
// Recreate all widgets in current array order
dsgnCreateWidgets(ds, parent);
}
// ============================================================
// syncWidgetGeom
// ============================================================
//
// Update the live widget's position and size from the design data.
static void syncWidgetGeom(DsgnControlT *ctrl) {
if (!ctrl->widget) {
return;
}
// Set minW/minH -- the layout engine uses these as the floor
// during the measure pass, so the arrange pass allocates this
// exact size (assuming weight=0).
ctrl->widget->minH = wgtPixels(ctrl->height);
ctrl->widget->prefH = wgtPixels(ctrl->height);
}

View file

@ -49,6 +49,7 @@ typedef struct {
int32_t tabIndex;
DsgnPropT props[DSGN_MAX_PROPS];
int32_t propCount;
WidgetT *widget; // live widget (created at design time for WYSIWYG)
} DsgnControlT;
// ============================================================
@ -62,6 +63,7 @@ typedef struct {
int32_t height;
DsgnControlT *controls; // stb_ds dynamic array
bool dirty;
WidgetT *contentBox; // VBox parent for live widgets
} DsgnFormT;
// ============================================================
@ -92,15 +94,10 @@ typedef enum {
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
HANDLE_E = 0, // resize width
HANDLE_S, // resize height
HANDLE_SE, // resize both
HANDLE_COUNT = 3
} DsgnHandleE;
// ============================================================
@ -109,9 +106,7 @@ typedef enum {
typedef enum {
DSGN_IDLE,
DSGN_PLACING,
DSGN_DRAWING,
DSGN_MOVING,
DSGN_REORDERING, // dragging control up/down in the VBox
DSGN_RESIZING
} DsgnModeE;
@ -125,17 +120,11 @@ typedef struct {
DsgnToolE activeTool;
DsgnModeE mode;
DsgnHandleE activeHandle;
int32_t dragStartX;
int32_t dragStartY;
int32_t dragOrigLeft;
int32_t dragOrigTop;
int32_t dragOrigWidth;
int32_t dragStartY; // mouse Y at drag start (for reorder)
int32_t dragOrigWidth; // widget size at resize start
int32_t dragOrigHeight;
int32_t drawX; // rubber-band start for new control
int32_t drawY;
bool showGrid;
bool snapToGrid;
WidgetT *canvas;
int32_t dragStartX; // mouse X at resize start
WindowT *formWin;
AppContextT *ctx;
} DsgnStateT;
@ -146,9 +135,13 @@ typedef struct {
// Initialize designer state.
void dsgnInit(DsgnStateT *ds, AppContextT *ctx);
// Set the canvas widget for rendering.
// Set the canvas overlay widget (for grab handles).
void dsgnSetCanvas(DsgnStateT *ds, WidgetT *canvas);
// Create live widgets for all controls in the form.
// Call after dsgnLoadFrm or dsgnNewForm, with the form window's contentBox.
void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox);
// Load a .frm file into the designer.
bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen);
@ -162,6 +155,9 @@ void dsgnNewForm(DsgnStateT *ds, const char *name);
// Repaint the design surface.
void dsgnPaint(DsgnStateT *ds);
// Draw selection handles over the painted window surface.
void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY);
// Handle mouse click on the design surface.
void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag);

View file

@ -8,15 +8,16 @@
// work on real hardware inside the DVX windowing system.
#include "dvxApp.h"
#include "dvxCursor.h"
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "dvxWidgetPlugin.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "widgetBox.h"
#include "widgetLabel.h"
#include "widgetTextInput.h"
#include "widgetDropdown.h"
#include "widgetCanvas.h"
#include "widgetSplitter.h"
#include "widgetStatusBar.h"
@ -89,7 +90,9 @@ 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 int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y);
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
static void onFormWinPaint(WindowT *win, RectT *dirtyArea);
static void updateDropdowns(void);
// ============================================================
@ -753,6 +756,13 @@ static void loadFile(void) {
snprintf(title, sizeof(title), "DVX BASIC - %s", path);
dvxSetTitle(sAc, sWin, title);
// Close any open form designer and clear cached form
if (sFormWin) {
onFormWinClose(sFormWin);
}
dsgnFree(&sDesigner);
updateDropdowns();
setStatus("File loaded.");
}
@ -1074,13 +1084,118 @@ static void printCallback(void *ctx, const char *text, bool newline) {
}
// ============================================================
// dsgnMouseCb
// onFormWinMouse
// ============================================================
//
// Handle mouse events on the form designer window. Coordinates
// are relative to the window's client area (content box origin).
// ============================================================
// onFormWinCursorQuery
// ============================================================
static void dsgnMouseCb(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
(void)w;
dsgnOnMouse(&sDesigner, cx, cy, drag);
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) {
(void)win;
if (!sDesigner.form || !sDesigner.form->controls) {
return 0;
}
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
if (sDesigner.selectedIdx < 0 || sDesigner.selectedIdx >= count) {
return 0;
}
DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx];
if (!ctrl->widget || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
return 0;
}
int32_t cx = ctrl->widget->x;
int32_t cy = ctrl->widget->y;
int32_t cw = ctrl->widget->w;
int32_t ch = ctrl->widget->h;
int32_t hs = DSGN_HANDLE_SIZE;
// SE handle (check first -- overlaps with E and S)
if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
return CURSOR_RESIZE_DIAG_NWSE;
}
// E handle (right edge center)
if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch/2 - hs/2 && y < cy + ch/2 + hs/2) {
return CURSOR_RESIZE_H;
}
// S handle (bottom center)
if (x >= cx + cw/2 - hs/2 && x < cx + cw/2 + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
return CURSOR_RESIZE_V;
}
return 0;
}
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
(void)win;
static int32_t lastButtons = 0;
bool wasDown = (lastButtons & MOUSE_LEFT) != 0;
bool isDown = (buttons & MOUSE_LEFT) != 0;
if (!sDesigner.form || !sFormWin) {
lastButtons = buttons;
return;
}
if (isDown) {
bool drag = wasDown;
dsgnOnMouse(&sDesigner, x, y, drag);
prpRefresh(&sDesigner);
if (sFormWin) {
dvxInvalidateWindow(sAc, sFormWin);
}
} else if (wasDown) {
dsgnOnMouse(&sDesigner, x, y, false);
prpRefresh(&sDesigner);
if (sFormWin) {
dvxInvalidateWindow(sAc, sFormWin);
}
}
lastButtons = buttons;
}
// ============================================================
// onFormWinPaint
// ============================================================
//
// Draw selection handles after widgets have painted.
static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
if (!win) {
return;
}
// Force a full measure + layout + paint cycle.
// widgetOnPaint normally skips relayout if root dimensions haven't
// changed, but we need it to pick up minH changes from handle drag.
if (win->widgetRoot) {
widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
win->widgetRoot->w = 0; // force layout pass to re-run
}
widgetOnPaint(win, dirtyArea);
// Then draw selection handles on top
int32_t winX = win->contentX;
int32_t winY = win->contentY;
dsgnPaintOverlay(&sDesigner, winX, winY);
}
@ -1091,7 +1206,7 @@ static void dsgnMouseCb(WidgetT *w, int32_t cx, int32_t cy, bool drag) {
static void onFormWinClose(WindowT *win) {
dvxDestroyWindow(sAc, win);
sFormWin = NULL;
dsgnSetCanvas(&sDesigner, NULL);
sDesigner.formWin = NULL;
if (sToolboxWin) {
tbxDestroy(sAc, sToolboxWin);
@ -1168,31 +1283,42 @@ static void switchToDesign(void) {
}
}
// Create the form designer window
// Create the form designer window (same size as runtime)
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);
sFormWin = dvxCreateWindowCentered(sAc, title, IDE_DESIGN_W, IDE_DESIGN_H, true);
if (!sFormWin) {
return;
}
sFormWin->onClose = onFormWinClose;
sDesigner.formWin = sFormWin;
WidgetT *root = wgtInitWindow(sAc, sFormWin);
WidgetT *canvas = wgtCanvas(root, formW, formH);
canvas->weight = 100;
wgtCanvasSetMouseCallback(canvas, dsgnMouseCb);
dsgnSetCanvas(&sDesigner, canvas);
WidgetT *contentBox = wgtVBox(root);
contentBox->weight = 100;
// Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint)
sFormWin->onPaint = onFormWinPaint;
sFormWin->onMouse = onFormWinMouse;
sFormWin->onCursorQuery = onFormWinCursorQuery;
// Create live widgets for each control
dsgnCreateWidgets(&sDesigner, contentBox);
// Set form caption as window title
if (sDesigner.form && sDesigner.form->caption[0]) {
char winTitle[280];
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDesigner.form->caption);
dvxSetTitle(sAc, sFormWin, winTitle);
}
// Shrink-wrap the window to its content (matches runtime behavior)
dvxFitWindow(sAc, sFormWin);
// Create toolbox and properties windows
if (!sToolboxWin) {
@ -1203,7 +1329,6 @@ static void switchToDesign(void) {
sPropsWin = prpCreate(sAc, &sDesigner);
}
dsgnPaint(&sDesigner);
setStatus("Design view open.");
}

View file

@ -1,15 +1,16 @@
// 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.
// A floating window with a TreeView listing all controls on the
// form (for selection and drag-reorder) and a read-only TextArea
// showing properties of the selected control.
#include "ideProperties.h"
#include "dvxDialog.h"
#include "dvxWm.h"
#include "widgetBox.h"
#include "widgetLabel.h"
#include "widgetTextInput.h"
#include "widgetTreeView.h"
#include "widgetSplitter.h"
#include <stdio.h>
#include <stdlib.h>
@ -21,8 +22,7 @@
// ============================================================
#define PRP_WIN_W 200
#define PRP_WIN_H 320
#define PRP_MAX_ROWS 64
#define PRP_WIN_H 340
// ============================================================
// Module state
@ -30,14 +30,17 @@
static DsgnStateT *sDs = NULL;
static WindowT *sPrpWin = NULL;
static WidgetT *sListArea = NULL; // TextArea showing properties as text
static WidgetT *sTree = NULL;
static WidgetT *sPropsArea = NULL;
static AppContextT *sPrpCtx = NULL;
static bool sUpdating = false; // prevent feedback loops
// ============================================================
// Prototypes
// ============================================================
static void onPrpClose(WindowT *win);
static void onTreeChange(WidgetT *w);
// ============================================================
// onPrpClose
@ -48,6 +51,111 @@ static void onPrpClose(WindowT *win) {
}
// ============================================================
// onTreeChange
// ============================================================
//
// Called when the TreeView selection changes or when items are
// reordered by drag. Sync the designer state from the tree.
static void onTreeChange(WidgetT *w) {
(void)w;
if (!sDs || !sDs->form || !sTree || sUpdating) {
return;
}
// Find which tree item is selected and map to control index
int32_t count = (int32_t)arrlen(sDs->form->controls);
int32_t selIdx = -1;
// Iterate tree items (children of sTree) to find the selected one
int32_t idx = 0;
for (WidgetT *item = sTree->firstChild; item; item = item->nextSibling, idx++) {
if (wgtTreeItemIsSelected(item)) {
selIdx = idx;
break;
}
}
// Check if the tree order differs from the design array
// (drag-reorder happened). If so, rebuild the array to match.
bool reordered = false;
idx = 0;
for (WidgetT *item = sTree->firstChild; item; item = item->nextSibling, idx++) {
if (idx >= count) {
break;
}
// Each tree item's userData stores the control name
const char *itemName = (const char *)item->userData;
if (itemName && strcmp(itemName, sDs->form->controls[idx].name) != 0) {
reordered = true;
break;
}
}
if (reordered) {
// Rebuild the controls array to match tree order
DsgnControlT *newArr = NULL;
idx = 0;
for (WidgetT *item = sTree->firstChild; item; item = item->nextSibling) {
const char *itemName = (const char *)item->userData;
if (!itemName) {
continue;
}
// Find this control in the original array
for (int32_t i = 0; i < count; i++) {
if (strcmp(sDs->form->controls[i].name, itemName) == 0) {
arrput(newArr, sDs->form->controls[i]);
break;
}
}
}
// Replace the controls array
arrfree(sDs->form->controls);
sDs->form->controls = newArr;
sDs->form->dirty = true;
// Rebuild live widgets in new order
if (sDs->form->contentBox) {
sDs->form->contentBox->firstChild = NULL;
sDs->form->contentBox->lastChild = NULL;
int32_t newCount = (int32_t)arrlen(sDs->form->controls);
for (int32_t i = 0; i < newCount; i++) {
sDs->form->controls[i].widget = NULL;
}
dsgnCreateWidgets(sDs, sDs->form->contentBox);
}
// Update selection to match tree
count = (int32_t)arrlen(sDs->form->controls);
}
// Update designer selection
if (selIdx != sDs->selectedIdx) {
sDs->selectedIdx = selIdx;
if (sDs->formWin) {
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
}
}
// Refresh properties display
prpRefresh(sDs);
}
// ============================================================
// prpCreate
// ============================================================
@ -56,7 +164,6 @@ 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);
@ -69,10 +176,18 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) {
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;
// Splitter: tree on top, properties on bottom
WidgetT *splitter = wgtSplitter(root, false);
splitter->weight = 100;
// Control tree (top pane)
sTree = wgtTreeView(splitter);
sTree->onChange = onTreeChange;
wgtTreeViewSetReorderable(sTree, true);
// Properties text (bottom pane)
sPropsArea = wgtTextArea(splitter, 4096);
sPropsArea->readOnly = true;
prpRefresh(ds);
return win;
@ -89,7 +204,8 @@ void prpDestroy(AppContextT *ctx, WindowT *win) {
}
sPrpWin = NULL;
sListArea = NULL;
sTree = NULL;
sPropsArea = NULL;
sDs = NULL;
}
@ -99,7 +215,38 @@ void prpDestroy(AppContextT *ctx, WindowT *win) {
// ============================================================
void prpRefresh(DsgnStateT *ds) {
if (!sListArea || !ds || !ds->form) {
if (!ds || !ds->form) {
return;
}
// Rebuild tree items
if (sTree) {
sUpdating = true;
// Clear existing tree items
sTree->firstChild = NULL;
sTree->lastChild = NULL;
int32_t count = (int32_t)arrlen(ds->form->controls);
for (int32_t i = 0; i < count; i++) {
DsgnControlT *ctrl = &ds->form->controls[i];
char label[128];
snprintf(label, sizeof(label), "%s (%s)", ctrl->name, ctrl->typeName);
WidgetT *item = wgtTreeItem(sTree, label);
item->userData = ctrl->name; // store name for reorder tracking
if (i == ds->selectedIdx) {
wgtTreeItemSetSelected(item, true);
}
}
sUpdating = false;
}
// Update properties text
if (!sPropsArea) {
return;
}
@ -109,11 +256,8 @@ void prpRefresh(DsgnStateT *ds) {
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, "Type = %s\n", ctrl->typeName);
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);
@ -123,12 +267,10 @@ void prpRefresh(DsgnStateT *ds) {
}
} 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);
wgtSetText(sPropsArea, buf);
}

View file

@ -58,7 +58,7 @@ static void onToolClick(WidgetT *w) {
if (toolIdx >= 0 && toolIdx < TOOL_COUNT) {
sDs->activeTool = (DsgnToolE)toolIdx;
sDs->mode = (toolIdx == TOOL_POINTER) ? DSGN_IDLE : DSGN_PLACING;
sDs->mode = DSGN_IDLE;
}
}