// 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; } }