DVX_GUI/widgets/wrapBox/widgetWrapBox.c
2026-04-04 15:42:27 -05:00

306 lines
7.6 KiB
C

#define DVX_WIDGET_IMPL
// widgetWrapBox.c -- Flow/wrap layout container
//
// Lays out children left-to-right, wrapping to the next row when
// the available width is exceeded. Each row's height is the max
// child height in that row. Supports spacing between items and
// rows. Supports per-row alignment (center, right) for short rows.
#include "dvxWidgetPlugin.h"
#include <stdlib.h>
#include <string.h>
// ============================================================
// Constants
// ============================================================
#define DEFAULT_PAD 2
#define DEFAULT_GAP 4
// ============================================================
// Type ID and per-instance data
// ============================================================
static int32_t sTypeId = -1;
typedef struct {
int32_t wrappedH; // actual height from last layout pass (-1 = not yet computed)
} WrapBoxDataT;
// ============================================================
// widgetWrapBoxCalcMinSize
// ============================================================
void widgetWrapBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t maxChildW = 0;
int32_t maxChildH = 0;
if (pad == 0) {
pad = DEFAULT_PAD;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
widgetCalcMinSizeTree(c, font);
if (c->calcMinW > maxChildW) {
maxChildW = c->calcMinW;
}
if (c->calcMinH > maxChildH) {
maxChildH = c->calcMinH;
}
}
w->calcMinW = maxChildW + pad * 2;
WrapBoxDataT *d = (WrapBoxDataT *)w->data;
if (d && d->wrappedH > 0) {
w->calcMinH = d->wrappedH;
} else {
w->calcMinH = maxChildH + pad * 2;
}
}
// ============================================================
// widgetWrapBoxLayout
// ============================================================
//
// Three-pass layout:
// 1. Place children left-to-right with wrapping
// 2. Apply per-row alignment offsets
// 3. Recurse into child containers (so children see final positions)
void widgetWrapBoxLayout(WidgetT *w, const BitmapFontT *font) {
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
if (pad == 0) {
pad = DEFAULT_PAD;
}
if (gap == 0) {
gap = DEFAULT_GAP;
}
int32_t availW = w->w - pad * 2;
int32_t baseX = w->x + pad;
// Pass 1: position children left-aligned with wrapping
int32_t curX = baseX;
int32_t curY = w->y + pad;
int32_t rowH = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t childW = c->calcMinW;
int32_t childH = c->calcMinH;
if (curX > baseX && (curX - baseX) + childW > availW) {
curX = baseX;
curY += rowH + gap;
rowH = 0;
}
c->x = curX;
c->y = curY;
c->w = childW;
c->h = childH;
if (childH > rowH) {
rowH = childH;
}
curX += childW + gap;
}
// Pass 2: apply alignment offsets per row
if (w->align == AlignCenterE || w->align == AlignEndE) {
WidgetT *rowStart = NULL;
int32_t rowY = -1;
int32_t rowEndX = 0;
for (WidgetT *c = w->firstChild; ; c = c ? c->nextSibling : NULL) {
bool newRow = false;
if (!c) {
newRow = (rowStart != NULL);
} else if (c->visible && c->y != rowY && rowStart != NULL) {
newRow = true;
}
if (newRow) {
int32_t rowW = rowEndX - baseX;
int32_t slack = availW - rowW;
if (slack > 0) {
int32_t offset = (w->align == AlignCenterE) ? slack / 2 : slack;
for (WidgetT *r = rowStart; r != c; r = r->nextSibling) {
if (r->visible && r->y == rowY) {
r->x += offset;
}
}
}
rowStart = NULL;
}
if (!c) {
break;
}
if (!c->visible) {
continue;
}
if (rowStart == NULL || c->y != rowY) {
rowStart = c;
rowY = c->y;
}
rowEndX = c->x + c->w;
}
}
// Pass 3: recurse into child containers AFTER alignment
// so children see their parent's final position.
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
widgetLayoutChildren(c, font);
}
}
// Store actual wrapped height for subsequent measure passes.
int32_t usedH = (curY + rowH + pad) - w->y;
WrapBoxDataT *d = (WrapBoxDataT *)w->data;
if (d) {
d->wrappedH = usedH;
}
if (usedH > w->calcMinH) {
w->calcMinH = usedH;
}
}
// ============================================================
// widgetWrapBoxPaint
// ============================================================
void widgetWrapBoxPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)w;
(void)disp;
(void)ops;
(void)font;
(void)colors;
}
// ============================================================
// DXE registration
// ============================================================
// ============================================================
// BASIC-facing accessors
// ============================================================
static int32_t wrapBoxGetAlign(const WidgetT *w) {
return (int32_t)w->align;
}
static void wrapBoxSetAlign(WidgetT *w, int32_t align) {
w->align = (WidgetAlignE)align;
wgtInvalidate(w);
}
static const char *sAlignNames[] = { "Left", "Center", "Right", NULL };
static const WgtPropDescT sProps[] = {
{ "Alignment", WGT_IFACE_ENUM, (void *)wrapBoxGetAlign, (void *)wrapBoxSetAlign, sAlignNames }
};
// ============================================================
// DXE registration
// ============================================================
static void wrapBoxDestroy(WidgetT *w) {
free(w->data);
}
static const WidgetClassT sClass = {
.version = WGT_CLASS_VERSION,
.flags = 0,
.handlers = {
[WGT_METHOD_PAINT] = (void *)widgetWrapBoxPaint,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetWrapBoxCalcMinSize,
[WGT_METHOD_LAYOUT] = (void *)widgetWrapBoxLayout,
[WGT_METHOD_DESTROY] = (void *)wrapBoxDestroy,
}
};
static WidgetT *wrapBoxCreate(WidgetT *parent) {
if (!parent) {
return NULL;
}
WidgetT *w = widgetAlloc(parent, sTypeId);
if (w) {
WrapBoxDataT *d = (WrapBoxDataT *)calloc(1, sizeof(WrapBoxDataT));
if (d) {
d->wrappedH = -1;
}
w->data = d;
}
return w;
}
static const struct {
WidgetT *(*create)(WidgetT *parent);
} sApi = {
.create = wrapBoxCreate
};
static const WgtIfaceT sIface = {
.basName = "WrapBox",
.props = sProps,
.propCount = 1,
.methods = NULL,
.methodCount = 0,
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT,
.isContainer = true,
.defaultEvent = "Click",
.namePrefix = "WrapBox"
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClass);
wgtRegisterApi("wrapbox", &sApi);
wgtRegisterIface("wrapbox", &sIface);
}