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