313 lines
8 KiB
C
313 lines
8 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 "dvxWgtP.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
|
|
// ============================================================
|
|
//
|
|
// Measure/layout cycle: the wrapped height depends on the allocated
|
|
// width, which isn't known during the measure pass. On the first
|
|
// measure, we report one row's height. The layout pass computes
|
|
// the actual wrapped height and stores it in WrapBoxDataT.wrappedH.
|
|
// On subsequent measures (triggered by resize or ScrollPane), we
|
|
// return the stored height so the parent sees the real content size.
|
|
|
|
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);
|
|
}
|