DVX_GUI/dvx/widgets/widgetLayout.c

388 lines
10 KiB
C

// widgetLayout.c — Layout engine (measure + arrange)
#include "widgetInternal.h"
// ============================================================
// widgetCalcMinSizeBox
// ============================================================
void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type);
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
int32_t mainSize = 0;
int32_t crossSize = 0;
int32_t count = 0;
if (pad == 0) {
pad = DEFAULT_PADDING;
}
if (gap == 0) {
gap = DEFAULT_SPACING;
}
// Frame: box starts at y + charHeight/2 so the title sits on the top line
int32_t frameExtraTop = 0;
if (w->type == WidgetFrameE) {
frameExtraTop = font->charHeight / 2;
pad = DEFAULT_PADDING;
}
// Toolbar and StatusBar use tighter padding
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
pad = 2;
gap = 2;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
widgetCalcMinSizeTree(c, font);
if (horiz) {
mainSize += c->calcMinW;
crossSize = DVX_MAX(crossSize, c->calcMinH);
} else {
mainSize += c->calcMinH;
crossSize = DVX_MAX(crossSize, c->calcMinW);
}
count++;
}
// Add spacing between children
if (count > 1) {
mainSize += gap * (count - 1);
}
// Add padding
mainSize += pad * 2;
crossSize += pad * 2;
if (horiz) {
w->calcMinW = mainSize;
w->calcMinH = crossSize + frameExtraTop;
} else {
w->calcMinW = crossSize;
w->calcMinH = mainSize + frameExtraTop;
}
// Frame border
if (w->type == WidgetFrameE) {
int32_t fb = widgetFrameBorderWidth(w);
w->calcMinW += fb * 2;
w->calcMinH += fb * 2;
}
}
// ============================================================
// widgetCalcMinSizeTree
// ============================================================
//
// Top-level measure dispatcher. Recurses through the widget tree.
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
if (widgetIsBoxContainer(w->type)) {
widgetCalcMinSizeBox(w, font);
} else if (w->type == WidgetTabControlE) {
widgetTabControlCalcMinSize(w, font);
} else if (w->type == WidgetTreeViewE) {
widgetTreeViewCalcMinSize(w, font);
} else {
// Leaf widgets
switch (w->type) {
case WidgetLabelE:
widgetLabelCalcMinSize(w, font);
break;
case WidgetButtonE:
widgetButtonCalcMinSize(w, font);
break;
case WidgetCheckboxE:
widgetCheckboxCalcMinSize(w, font);
break;
case WidgetRadioE:
widgetRadioCalcMinSize(w, font);
break;
case WidgetTextInputE:
widgetTextInputCalcMinSize(w, font);
break;
case WidgetSpacerE:
widgetSpacerCalcMinSize(w, font);
break;
case WidgetDropdownE:
widgetDropdownCalcMinSize(w, font);
break;
case WidgetImageE:
widgetImageCalcMinSize(w, font);
break;
case WidgetCanvasE:
widgetCanvasCalcMinSize(w, font);
break;
case WidgetComboBoxE:
widgetComboBoxCalcMinSize(w, font);
break;
case WidgetProgressBarE:
widgetProgressBarCalcMinSize(w, font);
break;
case WidgetSliderE:
widgetSliderCalcMinSize(w, font);
break;
case WidgetSeparatorE:
if (w->as.separator.vertical) {
w->calcMinW = SEPARATOR_THICKNESS;
w->calcMinH = 0;
} else {
w->calcMinW = 0;
w->calcMinH = SEPARATOR_THICKNESS;
}
break;
case WidgetTreeItemE:
w->calcMinW = 0;
w->calcMinH = 0;
break;
default:
w->calcMinW = 0;
w->calcMinH = 0;
break;
}
}
// Apply size hints (override calculated minimum)
if (w->minW) {
int32_t hintW = wgtResolveSize(w->minW, 0, font->charWidth);
if (hintW > w->calcMinW) {
w->calcMinW = hintW;
}
}
if (w->minH) {
int32_t hintH = wgtResolveSize(w->minH, 0, font->charWidth);
if (hintH > w->calcMinH) {
w->calcMinH = hintH;
}
}
}
// ============================================================
// widgetLayoutBox
// ============================================================
void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
bool horiz = widgetIsHorizContainer(w->type);
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
if (pad == 0) {
pad = DEFAULT_PADDING;
}
if (gap == 0) {
gap = DEFAULT_SPACING;
}
// Frame adjustments
int32_t frameExtraTop = 0;
int32_t fb = 0;
if (w->type == WidgetFrameE) {
frameExtraTop = font->charHeight / 2;
fb = widgetFrameBorderWidth(w);
pad = DEFAULT_PADDING;
}
// Toolbar and StatusBar use tighter padding
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
pad = 2;
gap = 2;
}
int32_t innerX = w->x + pad + fb;
int32_t innerY = w->y + pad + fb + frameExtraTop;
int32_t innerW = w->w - pad * 2 - fb * 2;
int32_t innerH = w->h - pad * 2 - fb * 2 - frameExtraTop;
if (innerW < 0) { innerW = 0; }
if (innerH < 0) { innerH = 0; }
int32_t count = widgetCountVisibleChildren(w);
if (count == 0) {
return;
}
int32_t totalGap = gap * (count - 1);
int32_t availMain = horiz ? (innerW - totalGap) : (innerH - totalGap);
int32_t availCross = horiz ? innerH : innerW;
// First pass: sum minimum sizes and total weight
int32_t totalMin = 0;
int32_t totalWeight = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
totalMin += cmin;
totalWeight += c->weight;
}
int32_t extraSpace = availMain - totalMin;
if (extraSpace < 0) {
extraSpace = 0;
}
// Compute alignment offset for main axis
int32_t alignOffset = 0;
if (totalWeight == 0 && extraSpace > 0) {
if (w->align == AlignCenterE) {
alignOffset = extraSpace / 2;
} else if (w->align == AlignEndE) {
alignOffset = extraSpace;
}
}
// Second pass: assign positions and sizes
int32_t pos = (horiz ? innerX : innerY) + alignOffset;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t cmin = horiz ? c->calcMinW : c->calcMinH;
int32_t mainSize = cmin;
// Distribute extra space by weight
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
mainSize += (extraSpace * c->weight) / totalWeight;
}
// Apply max size constraint
if (horiz && c->maxW) {
int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
if (mainSize > maxPx) {
mainSize = maxPx;
}
} else if (!horiz && c->maxH) {
int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
if (mainSize > maxPx) {
mainSize = maxPx;
}
}
// Assign geometry
if (horiz) {
c->x = pos;
c->y = innerY;
c->w = mainSize;
c->h = availCross;
} else {
c->x = innerX;
c->y = pos;
c->w = availCross;
c->h = mainSize;
}
// Apply preferred/max on cross axis
if (horiz && c->maxH) {
int32_t maxPx = wgtResolveSize(c->maxH, innerH, font->charWidth);
if (c->h > maxPx) {
c->h = maxPx;
}
} else if (!horiz && c->maxW) {
int32_t maxPx = wgtResolveSize(c->maxW, innerW, font->charWidth);
if (c->w > maxPx) {
c->w = maxPx;
}
}
pos += mainSize + gap;
// Recurse into child containers
widgetLayoutChildren(c, font);
}
}
// ============================================================
// widgetLayoutChildren
// ============================================================
//
// Top-level layout dispatcher.
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
if (widgetIsBoxContainer(w->type)) {
widgetLayoutBox(w, font);
} else if (w->type == WidgetTabControlE) {
widgetTabControlLayout(w, font);
} else if (w->type == WidgetTreeViewE) {
widgetTreeViewLayout(w, font);
}
}
// ============================================================
// wgtLayout
// ============================================================
void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT *font) {
if (!root) {
return;
}
// Measure pass
widgetCalcMinSizeTree(root, font);
// Layout pass
root->x = 0;
root->y = 0;
root->w = availW;
root->h = availH;
widgetLayoutChildren(root, font);
}
// ============================================================
// wgtResolveSize
// ============================================================
int32_t wgtResolveSize(int32_t taggedSize, int32_t parentSize, int32_t charWidth) {
if (taggedSize == 0) {
return 0;
}
uint32_t sizeType = (uint32_t)taggedSize & WGT_SIZE_TYPE_MASK;
int32_t value = taggedSize & WGT_SIZE_VAL_MASK;
switch (sizeType) {
case WGT_SIZE_PIXELS:
return value;
case WGT_SIZE_CHARS:
return value * charWidth;
case WGT_SIZE_PERCENT:
return (parentSize * value) / 100;
default:
return value;
}
}