Below is the full, integrated specification for the LayoutConstraints + SizeHints protocol, updated per your requirements:
int.MaxValueintStretch) are integrated correctlyAfter the spec, you'll find control-by-control guidance, grouped by shared behavior, plus recommended default alignments for your library.
Size and Rectangle types.You already have:
Size (integer width/height)
Rectangle (integer x/y/width/height)
This spec refers to conceptual members Width, Height, X, Y. Map to your actual names.
Unbounded / "infinite" maximum is represented by int.MaxValue.MUST: If any invariant in this spec is violated, throw a runtime exception (Release included).
Recommended exception types:
LayoutException : Exception for protocol violations (bad sizes, bad constraints, overflow).ArgumentOutOfRangeException for public API misuse (negative sizes, etc.).You asked to keep calculations in int. That's fine, but you must define what happens if arithmetic overflows.
MUST: Do arithmetic in a way that does not silently overflow.
You have two valid options (pick one and enforce it consistently):
Option A (recommended): checked arithmetic + LayoutException
checked context.LayoutException.Option B: saturating arithmetic
int.MaxValue.Natural == int.MaxValue).This spec assumes Option A (checked + throw). (You can switch to saturating later, but be consistent.)
LayoutConstraintsRepresents the allowable size range for a node.
Fields (conceptual):
MinWidth, MaxWidthMinHeight, MaxHeightInvariants (MUST):
0 <= MinWidth <= MaxWidth <= int.MaxValue0 <= MinHeight <= MaxHeight <= int.MaxValueDerived:
IsWidthBounded := MaxWidth < int.MaxValueIsHeightBounded := MaxHeight < int.MaxValueNotes (implementation detail):
int.MaxValue is reserved as the unbounded sentinel and must never be used as a finite size.MaxFinite = int.MaxValue - 1 as the largest finite size allowed for Min/Natural.SizeHintsReturned by Measure.
Fields (conceptual):
Min : Size (finite)Natural : Size (finite, preferred)Max : Size (may contain int.MaxValue)FlexGrowX : int (>= 0)FlexGrowY : int (>= 0)FlexShrinkX : int (>= 0)FlexShrinkY : int (>= 0)Invariants (MUST), per axis:
0 <= Min <= Natural <= Max <= int.MaxValueNatural.Width and Natural.Height MUST be finite (<= int.MaxValue - 1)Min.Width and Min.Height MUST be finite (<= int.MaxValue - 1)Interpretation:
Min: smallest usable sizeNatural: shrink-wrap / preferred sizeMax: largest meaningful size (can be unbounded)Each node exposes:
HorizontalAlignment ∈ { Start, Center, End, Stretch }VerticalAlignment ∈ { Start, Center, End, Stretch }Default framework alignment (your current default):
Align.StartAlign.StartThis spec integrates alignment as an arrange-time policy.
public class Visual
{
public SizeHints Measure(in LayoutConstraints constraints) //...
protected virtual SizeHints MeasureCore(in LayoutConstraints constraints) //...
public void Arrange(Rectangle finalRect) //...
protected virtual void ArrangeCore(in Rectangle finalRect)
}
Notes:
public void Measure(Size availableSize) as a convenience wrapper for callers that only have a max size.
It must not be used by controls internally: control layout code must always call Measure(in LayoutConstraints) on children so that
constraints remain explicit and unboundedness is never confused with “request infinity”.When Measure(constraints) is called, a node:
SizeHints satisfying invariants.Natural (or Min) to int.MaxValue.Critical rule (prevents your bug):
"Stretch/Fill" MUST NOT be implemented by returning
Natural = constraints.Max. Stretch is handled during Arrange by how the parent sizes the child's slot.
Measure is allowed to be called multiple times with different constraints. Results must be deterministic for the same inputs.
When Arrange(finalRect) is called, a node:
Nodes MAY re-measure children during Arrange only for dependency resolution (e.g., text wrapping based on final width). Such internal re-measure must not cause recursive relayout of ancestors.
A parent container computes a slot rectangle for each child. Then it applies the child's alignment and hints to produce the final rectangle passed to child.Arrange.
Given:
slot : Rectangle (finite)hints : SizeHintsAlignX, AlignYCompute final child width/height:
Horizontal size
If AlignX == Stretch:
childW = slot.WidthElse:
childW = min(hints.Natural.Width, slot.Width)Then clamp to capability:
childW = clamp(childW, hints.Min.Width, min(hints.Max.Width, slot.Width))Vertical size
If AlignY == Stretch:
childH = slot.HeightElse:
childH = min(hints.Natural.Height, slot.Height)Clamp:
childH = clamp(childH, hints.Min.Height, min(hints.Max.Height, slot.Height))Now compute position:
Horizontal position
Left: dx = 0Center: dx = (slot.Width - childW) / 2Right: dx = slot.Width - childWStretch: dx = 0Vertical position
Top: dy = 0Center: dy = (slot.Height - childH) / 2Bottom: dy = slot.Height - childHStretch: dy = 0Final rect:
childRect = (slot.X + dx, slot.Y + dy, childW, childH)Important consequence: A leaf can be "Stretch" without ever returning "available" from Measure. Unbounded constraints are safe.
Containers that distribute space along one axis (HStack/VStack, some grids, etc.) use flex allocation.
Per child i, choose a "base" size along the main axis:
base[i] = hints.Natural.(Axis)Then:
sum(base) < available distribute extra to children with FlexGrow > 0 (weight-based)sum(base) > available distribute deficit by shrinking children with FlexShrink > 0 down to Min (weight-based)Rules (MUST):
Min or above Max.Note: Alignment is applied after the slot is computed. Flex alloc decides slot sizes; alignment decides how the child uses them.
In this framework, Stretch alignment implies “willingness to grow” on that axis when a parent runs a flex allocation algorithm.
Normative behavior:
Visual with HorizontalAlignment == Stretch must be treated as having FlexGrowX > 0 (typically 1) when participating in flex allocation.Visual with VerticalAlignment == Stretch must be treated as having FlexGrowY > 0 (typically 1) when participating in flex allocation.Implementation note:
Visual.Measure(...) wrapper adjusting the returned SizeHints from MeasureCore(...) so that flex containers
(HStack/VStack/etc.) can allocate remaining space to stretched children without requiring children to “ask for available size” in Measure.ScrollViewer must distinguish:
Given parent constraints, define childConstraints:
If horizontal scrolling enabled:
childConstraints.MaxWidth = int.MaxValuechildConstraints.MaxWidth = constraints.MaxWidthIf vertical scrolling enabled:
childConstraints.MaxHeight = int.MaxValuechildConstraints.MaxHeight = constraints.MaxHeightCall content.Measure(childConstraints) and set:
extent = contentHints.Natural (must be finite)Then ScrollViewer hints:
Per axis:
If parent axis is bounded:
Natural = min(extent, constraints.Max) (don't ask bigger than viewport)If parent axis is unbounded:
Natural = extent (no reason to scroll; can expand)ScrollViewer typically reports:
Max = constraints.Max (it shouldn't exceed what parent allows)Given finalRect (viewport):
Arrange content at size:
contentW = max(extent.Width, finalRect.Width) if you want "stretch if smaller"contentW = extent.WidthPlace with offsets:
contentX = finalRect.X - ScrollXcontentY = finalRect.Y - ScrollYClip rendering to finalRect.
Content alignment inside ScrollViewer: If content is smaller than viewport and you want centering, apply alignment between viewport slot and content rect using the alignment rules from §6.
Some nodes' height depends on width (TextBlock with wrapping, TextArea, Table, etc.)
Rules:
In Measure: if constraints.MaxWidth is bounded, compute wrapped height using that width.
In Arrange: parent may re-measure with a tight width constraint:
MinWidth = MaxWidth = allocatedWidthAny such internal re-measure must not invalidate ancestors during the same pass.
The layout engine MUST validate at runtime (Release included):
LayoutConstraints invariantsSizeHints invariantsNatural and Min are never int.MaxValueOn failure: throw LayoutException (or your chosen equivalent).
Below, I assume your general default is Horizontal=Left, Vertical=Top. I'll only call out controls that should override to better defaults.
I'll group controls by how they should respect the spec: leaf, decorator, layout container, scrolling/virtualized, overlay, etc.
Natural.Height = 1, Natural.Width = 0 (finite). Max.Width = int.MaxValue.FlexGrowX = 1 is often appropriate (acts like spacer line in HStack/VStack).Natural.Width = constraints.MaxWidth.If rendered as a 1-row component: Natural.Height = 1.
Sparkline often benefits from taking available width:
Natural.Width finite (e.g., minimal 1 or 0), Max.Width = int.MaxValue, FlexGrowX = 1.Natural.Height = 1.Max.Width = int.MaxValue, and typically FlexGrowX = 1.Natural finite: choose a sensible default like (minW, minH) (e.g., (10, 4)), never int.MaxValue.Measure:
Natural.Width = text length, Natural.Height = 1.Natural.Width <= MaxWidth, Natural.Height = wrapped lines.Max.Width can be int.MaxValue (clipping/truncation allowed).
Default alignment: Left/Top is fine.
Natural.Height = 1, Natural.Width finite (e.g., minimal 4–10 or based on placeholder).Max.Width = int.MaxValue, FlexGrowX = 1 common.Natural finite (e.g., (20, 5)), Max unbounded, flex-grow both axes often 1.Natural.Height = 1This is a container whose job is to center its child.
Arrange: child slot = full rect, but force child alignment behavior:
Default alignment: Center should usually be Stretch/Stretch as a container (it wants full area to center within).
Treat as wrappers:
Default alignment: follow framework default (Left/Top).
(0,0)), max unbounded.Must implement the flex allocator (§7) on the main axis.
Cross-axis:
Default alignment of the container itself: typically Stretch/Stretch (containers usually want to occupy offered area).
Docked children consume edges; remaining rect goes to fill child.
Alignment matters for children within their dock slot.
Typical defaults:
Grid is the main consumer of Min/Natural/Max.
Recommended approach:
Apply child alignment inside each cell slot (§6).
Default: container Stretch/Stretch.
Very grid-like; column widths depend on content.
When measured unbounded, Natural widths may grow with data; keep it finite:
Usually acts like a viewport:
Natural = int.MaxValue.Orientation-specific:
Natural.Height = 1, Horizontal = StretchNatural.Width = 1, Vertical = StretchThe non-length axis should be fixed (Max = 1) so Stretch doesn't bloat thickness.
These are similar in layout behavior. Grouping:
OptionListItem as the item type)Key points:
Items are typically Height = 1 each (tree may have indentation).
If measured with unbounded height, reporting full extent is okay but must stay finite:
itemCount (or visible computed height), with overflow protection.Most of these behave as viewports:
Virtualization (optional but recommended):
Key points:
(0,0) or minimal header-only size.Key points:
Natural.Height = 1Max.Width = int.MaxValue, FlexGrowX = 1Menu items inside:
MenuItem: row height 1, usually horizontal stretch within the bar or popup menu.Key points:
These are usually placed by an overlay host (often a ZStack/Layer).
Recommended defaults:
Natural derived from content, clamped to viewport constraintsPopups may prefer:
Implementation:
Key points:
Thickness is fixed (usually 1 cell):
Natural.Height = 1, Max.Height = 1, Horizontal = StretchNatural.Width = 1, Max.Width = 1, Vertical = StretchThis ensures Stretch never inflates thickness.
Buttons:
Keep global defaults Left/Top, but override these for better UX:
Stretch horizontally by default
OptionListItem, selection/list itemsStretch both axes by default (viewport/container-like)
Center by default (overlay-like)
If you want, I can also provide a single reusable helper in your codebase:
ApplyAlignment(slotRect, hints, hAlign, vAlign) -> RectangleAllocateFlexAxis(...) functionBoth would encode §6 + §7 in one place so every container uses identical, bug-free behavior.