388 lines
10 KiB
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;
|
|
}
|
|
}
|