433 lines
14 KiB
C
433 lines
14 KiB
C
// widgetLayout.c — Layout engine (measure + arrange)
|
|
//
|
|
// Implements a two-pass layout algorithm inspired by CSS flexbox:
|
|
// Pass 1 (Measure): bottom-up calculation of each widget's minimum
|
|
// size. Leaf widgets report their intrinsic size (text width, etc.)
|
|
// and containers sum their children's minimums plus padding/spacing.
|
|
// Pass 2 (Arrange): top-down assignment of actual positions and sizes.
|
|
// The root gets the full available area, then each container divides
|
|
// its space among children based on their minimums and weight values.
|
|
//
|
|
// Why two passes instead of one:
|
|
// Containers can't assign sizes until they know all children's minimums
|
|
// (to calculate extra space for weight distribution). And children can't
|
|
// know their minimums until their subtrees are measured. So measure must
|
|
// be bottom-up before arrange can be top-down.
|
|
//
|
|
// Why flexbox-like rather than constraint-based:
|
|
// Flexbox maps naturally to the VBox/HBox container model — it's simple
|
|
// to understand, implement, and debug. Constraint-based layout (like
|
|
// Cassowary) would add significant complexity for little benefit in a
|
|
// DOS GUI where most layouts are linear stacks of widgets.
|
|
//
|
|
// Size hints use a tagged integer encoding (see dvxWidget.h):
|
|
// High 2 bits encode the unit type (pixels, characters, percent),
|
|
// low 30 bits encode the numeric value. This packs three distinct
|
|
// unit types into a single int32_t without needing a separate struct.
|
|
// The wgtResolveSize() function decodes the tag and converts to pixels.
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
|
|
// ============================================================
|
|
// widgetCalcMinSizeBox
|
|
// ============================================================
|
|
//
|
|
// Measure pass for box containers (VBox, HBox, RadioGroup, StatusBar,
|
|
// Toolbar, Frame, TabPage). Recursively measures all visible children,
|
|
// then computes this container's minimum size as:
|
|
// main axis: sum of children minimums + gaps + padding
|
|
// cross axis: max of children minimums + padding
|
|
//
|
|
// "Main axis" is vertical for VBox, horizontal for HBox. The horiz
|
|
// flag flips the calculation so VBox and HBox share the same code.
|
|
|
|
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 reserves extra vertical space at the top so the frame title
|
|
// text can sit centered on the top border line (like Windows group boxes).
|
|
// Without this, the first child would overlap the title.
|
|
int32_t frameExtraTop = 0;
|
|
|
|
if (w->type == WidgetFrameE) {
|
|
frameExtraTop = font->charHeight / 2;
|
|
pad = DEFAULT_PADDING;
|
|
}
|
|
|
|
// Toolbar and StatusBar use minimal padding to pack controls tightly,
|
|
// matching the compact feel of classic Windows/Motif toolbars.
|
|
if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) {
|
|
pad = TOOLBAR_PAD;
|
|
gap = TOOLBAR_GAP;
|
|
}
|
|
|
|
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. Routes to the appropriate measure
|
|
// function based on widget type:
|
|
// - Box containers use the generic widgetCalcMinSizeBox()
|
|
// - Widgets with custom layout (TabControl, TreeView, ScrollPane,
|
|
// Splitter) provide their own calcMinSize via the vtable
|
|
// - Widgets without calcMinSize get 0x0 (they rely on size hints)
|
|
//
|
|
// After the type-specific measure, explicit minW/minH size hints
|
|
// are applied as a floor. This allows "this widget should be at
|
|
// least N pixels/chars wide" without changing the widget's natural
|
|
// size calculation. Hints only increase the minimum, never shrink it.
|
|
|
|
void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font) {
|
|
if (widgetIsBoxContainer(w->type)) {
|
|
widgetCalcMinSizeBox(w, font);
|
|
} else if (w->wclass && w->wclass->calcMinSize) {
|
|
w->wclass->calcMinSize(w, font);
|
|
} else {
|
|
w->calcMinW = 0;
|
|
w->calcMinH = 0;
|
|
}
|
|
|
|
// Size hints act as a floor: if the app specified wgtPixels(200) as
|
|
// minW, the widget will be at least 200px wide even if its intrinsic
|
|
// content is smaller. This is resolved at measure time (not arrange)
|
|
// so parent containers see the correct minimum when distributing space.
|
|
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
|
|
// ============================================================
|
|
//
|
|
// Arrange pass for box containers. This is the core of the flexbox-
|
|
// like layout algorithm, working in two sub-passes:
|
|
//
|
|
// Sub-pass 1: Sum all children's minimum sizes and total weight.
|
|
// "Extra space" = available main-axis size - total minimum sizes.
|
|
//
|
|
// Sub-pass 2: Assign each child's position and size.
|
|
// - Each child gets at least its minimum size.
|
|
// - Extra space is distributed proportionally by weight (like CSS
|
|
// flex-grow). A child with weight=100 gets twice as much extra
|
|
// as one with weight=50.
|
|
// - If total weight is 0 (all children are fixed-size), extra space
|
|
// is distributed according to the container's alignment: start
|
|
// (default), center, or end.
|
|
// - maxW/maxH constraints cap a child's final size.
|
|
// - Cross-axis size is the full available cross dimension (minus
|
|
// any maxW/maxH constraint on that axis).
|
|
//
|
|
// After positioning each child, widgetLayoutChildren() recurses to
|
|
// lay out the child's own subtree.
|
|
|
|
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 = TOOLBAR_PAD;
|
|
gap = TOOLBAR_GAP;
|
|
}
|
|
|
|
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;
|
|
|
|
// Sub-pass 1: accumulate children's minimum sizes and weights.
|
|
// totalMin is the space needed if every child gets exactly its minimum.
|
|
// totalWeight determines how to split any leftover space.
|
|
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;
|
|
}
|
|
|
|
// Alignment only applies when no children have weight (all fixed-size).
|
|
// When weights exist, the extra space is consumed by weighted children
|
|
// and alignment has no effect.
|
|
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. Mirrors the measure dispatcher
|
|
// in widgetCalcMinSizeTree(): box containers use the generic
|
|
// algorithm, widgets with custom layout (TabControl, TreeView,
|
|
// ScrollPane, Splitter) use their vtable entry, and leaf widgets
|
|
// do nothing (they have no children to lay out).
|
|
|
|
void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font) {
|
|
if (widgetIsBoxContainer(w->type)) {
|
|
widgetLayoutBox(w, font);
|
|
} else if (w->wclass && w->wclass->layout) {
|
|
w->wclass->layout(w, font);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtLayout
|
|
// ============================================================
|
|
//
|
|
// Public entry point: runs both passes on the entire widget tree.
|
|
// The root widget is positioned at (0,0) and given the full available
|
|
// area, then the arrange pass distributes space to its children.
|
|
//
|
|
// This is called from widgetManageScrollbars() and widgetOnPaint(),
|
|
// which may pass a virtual content size larger than the physical
|
|
// window if scrolling is needed.
|
|
|
|
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
|
|
// ============================================================
|
|
//
|
|
// Decodes a tagged size value into an actual pixel count.
|
|
//
|
|
// The tagged integer format uses the high 2 bits as a type tag:
|
|
// 00 = pixels (value is used directly)
|
|
// 01 = characters (value * charWidth, for text-relative sizing)
|
|
// 10 = percent (value% of parentSize, for responsive layouts)
|
|
//
|
|
// This encoding allows a single int32_t field to represent any of
|
|
// three unit types without needing a separate struct or enum.
|
|
// The tradeoff is that pixel values are limited to 30 bits (~1 billion),
|
|
// which is far more than any supported display resolution.
|
|
//
|
|
// A value of 0 means "auto" (use intrinsic/calculated size) — the
|
|
// caller checks for 0 before calling this function.
|
|
|
|
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;
|
|
}
|
|
}
|