This document captures design and implementation notes for the wrapping stack layout controls:
WrapHStack (horizontal flow; wraps into rows)WrapVStack (vertical flow; wraps into columns)For end-user usage and examples, see WrapStack.
Start/Center/End/Space*)MeasureMode)ArrayPool<T>WrapHStack : WrapStackBaseWrapVStack : WrapStackBaseWrapStackBase is an internal implementation base (EditorBrowsable(Never)); prefer the concrete controls.
All values below are [Bindable] on WrapStackBase:
Spacing : int
0.RunSpacing : int
0.Justify : WrapJustify
MeasureMode : WrapMeasureMode
WrapJustify:
StartCenterEndSpaceBetweenSpaceAroundSpaceEvenlyWrapMeasureMode:
ConstrainToRun (default)UnconstrainedWrapHStack defaults HorizontalAlignment = Align.Start (shrink-wrap on X unless parent stretches it).WrapVStack defaults VerticalAlignment = Align.Start (shrink-wrap on Y unless parent stretches it).Justify = WrapJustify.StartMeasureMode = WrapMeasureMode.ConstrainToRunSpacing = 0, RunSpacing = 0Wrap stacks define:
For WrapHStack:
For WrapVStack:
Each child is measured before runs are built:
MeasureMode == Unconstrained, children are measured with MaxMain = ∞.MeasureMode == ConstrainToRun, children are measured with MaxMain = constraints.MaxMain.This exists to support text-like visuals where height depends on width:
ConstrainToRun: lets a TextBlock/Markup wrap to the available widthUnconstrained: lets items keep their intrinsic width and overflow is expected to be clippedRuns are built using natural main-axis sizes (MeasureHints.Natural):
runMain + Spacing + childMain <= availableMainSpecial cases:
availableMain <= 0: each child becomes its own run (deterministic ordering; everything overflows)availableMain == ∞: all children are placed into a single runFor each run, the implementation tracks:
MainNatural, MainMinCrossNatural, CrossMinMaxCrossInfinite)The wrap stack’s reported sizes:
NaturalMain = max(run.MainNatural)NaturalCross = sum(run.CrossNatural) + RunSpacing * (runs - 1)MinMain = max(run.MainMin) (clamped to NaturalMain)MinCross = sum(run.CrossMin) + RunSpacing * (runs - 1) (clamped to NaturalCross)MaxMain = ∞MaxCross = ∞ if any run has MaxCrossInfinite, otherwise NaturalCrossThe result is converted back to (Width, Height) depending on orientation.
ConstrainToRun supports a common terminal UI pattern:
If MeasureMode == ConstrainToRun and the final main-axis size differs from the main-axis size used when runs were last computed,
children are re-measured with MaxMain = finalMain before run building.
Within each run, the available main-axis size is:
available = finalMain - Spacing * (itemCount - 1)The run then allocates per-item sizes using Layout.FlexAllocator.Allocate(...) with each child’s:
Min, Natural, MaxFlexGrow, FlexShrinkThis makes wrapping stacks consistent with other layout containers for “stretch/shrink” behavior.
The cross-axis size of a run is:
runCross = run.CrossNatural (max natural cross of children in the run)Children are arranged into slots of:
WrapHStack: (width = allocatedMain, height = runCross)WrapVStack: (width = runCross, height = allocatedMain)Child self-alignment then positions/sizes it inside its slot (Align.Start/Center/End/Stretch).
After allocation, any remaining space on the main axis is distributed using Justify:
Start: no offset; keep SpacingCenter: leading offset = leftover / 2End: leading offset = leftoverSpaceBetween: add extra to gaps between items (no leading/trailing padding)SpaceEvenly: distribute including start/end edgesSpaceAround: distribute around items (start/end edges get half of the between-item space)The implementation keeps a deterministic result without extra allocations by:
leftover % gaps) as +1 to the first N gapsTo keep Arrange allocation-conscious, the implementation:
ArrayPool<int> for:
O(n) over children and uses cached results keyed on:
Children.Version)Spacing, RunSpacing, Justify, MeasureModeTests that lock down current behavior:
src/XenoAtom.Terminal.UI.Tests/WrapHStackLayoutTests.cssrc/XenoAtom.Terminal.UI.Tests/WrapVStackLayoutTests.csNotable covered scenarios:
Spacing and RunSpacingCenter, End)MeasureMode.Unconstrained allowing main-axis overflow