diff --git a/core/dvxApp.c b/core/dvxApp.c index 50bc236..52d21e3 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -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; diff --git a/core/dvxTypes.h b/core/dvxTypes.h index ad536f7..27147c7 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -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; // ============================================================ diff --git a/dvxbasic/ide/ideDesigner.c b/dvxbasic/ide/ideDesigner.c index fee8e93..8085e37 100644 --- a/dvxbasic/ide/ideDesigner.c +++ b/dvxbasic/ide/ideDesigner.c @@ -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 const char *getPropValue(const DsgnControlT *ctrl, const char *name); +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 void setPropValue(DsgnControlT *ctrl, const char *name, const char *value); +static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y); +static const char *resolveTypeName(const char *typeName); +static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value); +static void rebuildWidgets(DsgnStateT *ds); +static void syncWidgetGeom(DsgnControlT *ctrl); + + +// ============================================================ +// createDesignWidget +// ============================================================ +// +// Create a real DVX widget for design-time display. Mirrors the +// logic in formrt.c createWidget(). + +static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) { + const char *wgtName = resolveTypeName(vbTypeName); + + if (!wgtName) { + return NULL; + } + + const void *api = wgtGetApi(wgtName); + + if (!api) { + return NULL; + } + + typedef WidgetT *(*CreateParentFnT)(WidgetT *); + typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *); + typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t); + typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool); + + if (strcasecmp(wgtName, "button") == 0 || + strcasecmp(wgtName, "checkbox") == 0 || + strcasecmp(wgtName, "label") == 0) { + CreateParentTextFnT fn = *(CreateParentTextFnT *)api; + return fn(parent, ""); + } + + if (strcasecmp(wgtName, "textinput") == 0 || + strcasecmp(wgtName, "combobox") == 0) { + CreateParentIntFnT fn = *(CreateParentIntFnT *)api; + return fn(parent, DEFAULT_CREATE_ARG); + } + + if (strcasecmp(wgtName, "timer") == 0) { + CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api; + return fn(parent, 1000, false); + } + + if (strcasecmp(wgtName, "listbox") == 0) { + CreateParentFnT fn = *(CreateParentFnT *)api; + return fn(parent); + } + + // Fallback: try simple create(parent) + CreateParentFnT fn = *(CreateParentFnT *)api; + return fn(parent); +} // ============================================================ @@ -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"; } @@ -99,9 +154,9 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t else if (strcasecmp(typeName, "VScrollBar") == 0) { prefix = "VScroll"; } else if (strcasecmp(typeName, "PictureBox") == 0) { prefix = "Picture"; } - int32_t highest = 0; + int32_t highest = 0; int32_t prefixLen = (int32_t)strlen(prefix); - int32_t count = ds->form ? (int32_t)arrlen(ds->form->controls) : 0; + 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) { @@ -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 // ============================================================ @@ -151,13 +250,11 @@ void dsgnFree(DsgnStateT *ds) { void dsgnInit(DsgnStateT *ds, AppContextT *ctx) { memset(ds, 0, sizeof(*ds)); - ds->selectedIdx = -1; - ds->activeTool = TOOL_POINTER; - ds->mode = DSGN_IDLE; + ds->selectedIdx = -1; + ds->activeTool = TOOL_POINTER; + ds->mode = DSGN_IDLE; ds->activeHandle = HANDLE_NONE; - ds->showGrid = true; - ds->snapToGrid = true; - ds->ctx = ctx; + ds->ctx = ctx; } @@ -178,9 +275,9 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { return false; } - form->controls = NULL; - form->width = DEFAULT_FORM_W; - form->height = DEFAULT_FORM_H; + 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"); @@ -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; } @@ -261,8 +355,8 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { 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.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]; @@ -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,49 +409,26 @@ 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); } - else if (strcasecmp(key, "Width") == 0) { curCtrl->width = atoi(val); } - else if (strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); } + 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); } + 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; @@ -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; + ctrl->width = ds->dragOrigWidth + dx; 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,163 +507,193 @@ 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); + if (next->widget && y > next->widget->y + next->widget->h / 2) { + DsgnControlT tmp = ds->form->controls[ds->selectedIdx]; + ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1]; + ds->form->controls[ds->selectedIdx + 1] = tmp; + rebuildWidgets(ds); + ds->selectedIdx++; + ds->dragStartY = y; + ds->form->dirty = true; + } + } + } else if (dy < 0 && ctrl->widget) { + // Dragging up -- swap with previous control if past its midpoint + if (ds->selectedIdx > 0) { + DsgnControlT *prev = &ds->form->controls[ds->selectedIdx - 1]; + + if (prev->widget && y < prev->widget->y + prev->widget->h / 2) { + DsgnControlT tmp = ds->form->controls[ds->selectedIdx]; + ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1]; + ds->form->controls[ds->selectedIdx - 1] = tmp; + rebuildWidgets(ds); + ds->selectedIdx--; + ds->dragStartY = y; + ds->form->dirty = true; + } + } } } return; } - // Mouse click (not drag) - - // 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) { + // 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; + // Pointer tool: select, start resize or reorder + if (ds->activeTool == TOOL_POINTER) { + // Check grab handles of selected control first + if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { + DsgnHandleE handle = hitTestHandles(&ds->form->controls[ds->selectedIdx], x, y); + + if (handle != HANDLE_NONE) { + DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + ds->mode = DSGN_RESIZING; + ds->activeHandle = handle; + ds->dragStartX = x; + ds->dragStartY = y; + ds->dragOrigWidth = ctrl->widget ? ctrl->widget->w : ctrl->width; + ds->dragOrigHeight = ctrl->widget ? ctrl->widget->h : ctrl->height; + return; + } + } + + // Hit test controls -- click to select, drag to reorder + int32_t hit = hitTestControl(ds, x, y); + + if (hit >= 0) { + ds->selectedIdx = hit; + ds->mode = DSGN_REORDERING; + ds->dragStartY = y; + } else { + ds->selectedIdx = -1; + } + return; } - // Pointer tool: select and start drag + // 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); - // Check grab handles of selected control first - if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { - DsgnHandleE handle = hitTestHandles(&ds->form->controls[ds->selectedIdx], x, y); + // Create the live widget + if (ds->form->contentBox) { + ctrl.widget = createDesignWidget(typeName, ds->form->contentBox); - 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; + 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); } } - // 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); + 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); } diff --git a/dvxbasic/ide/ideDesigner.h b/dvxbasic/ide/ideDesigner.h index 29f9f0d..b1b98ac 100644 --- a/dvxbasic/ide/ideDesigner.h +++ b/dvxbasic/ide/ideDesigner.h @@ -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); diff --git a/dvxbasic/ide/ideMain.c b/dvxbasic/ide/ideMain.c index e622f37..07e5cfc 100644 --- a/dvxbasic/ide/ideMain.c +++ b/dvxbasic/ide/ideMain.c @@ -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); - prpRefresh(&sDesigner); +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; + 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 *root = wgtInitWindow(sAc, sFormWin); + 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."); } diff --git a/dvxbasic/ide/ideProperties.c b/dvxbasic/ide/ideProperties.c index 055e688..ec4ff0f 100644 --- a/dvxbasic/ide/ideProperties.c +++ b/dvxbasic/ide/ideProperties.c @@ -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 #include @@ -21,23 +22,25 @@ // ============================================================ #define PRP_WIN_W 200 -#define PRP_WIN_H 320 -#define PRP_MAX_ROWS 64 +#define PRP_WIN_H 340 // ============================================================ // Module state // ============================================================ -static DsgnStateT *sDs = NULL; -static WindowT *sPrpWin = NULL; -static WidgetT *sListArea = NULL; // TextArea showing properties as text -static AppContextT *sPrpCtx = NULL; +static DsgnStateT *sDs = NULL; +static WindowT *sPrpWin = NULL; +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; @@ -88,9 +203,10 @@ void prpDestroy(AppContextT *ctx, WindowT *win) { dvxDestroyWindow(ctx, win); } - sPrpWin = NULL; - sListArea = NULL; - sDs = NULL; + sPrpWin = 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); } diff --git a/dvxbasic/ide/ideToolbox.c b/dvxbasic/ide/ideToolbox.c index e851fbf..104dc6a 100644 --- a/dvxbasic/ide/ideToolbox.c +++ b/dvxbasic/ide/ideToolbox.c @@ -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; } }