Editor/Canvas/SuiFlexLayout.cs
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;
using SboxUiDesigner.Runtime;
namespace SboxUiDesigner.EditorUi.Canvas;
/// <summary>
/// Flex layout pass — Yoga-subset implementation.
///
/// Supported (Phase 3 MVP):
/// - <c>flex-direction</c>: Row, Column, RowReverse, ColumnReverse
/// - <c>justify-content</c>: FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround, SpaceEvenly
/// - <c>align-items</c>: FlexStart, Center, FlexEnd, Stretch
/// - <c>gap</c>
/// - <c>margin</c> / <c>padding</c>
///
/// NOT supported (V2):
/// - <c>flex-grow</c>, <c>flex-shrink</c>, <c>flex-basis</c> (children always use intrinsic size)
/// - <c>flex-wrap</c> (single line only)
/// - <c>align-self</c> (inherits parent's align-items)
/// - Baseline align (collapses to FlexStart)
///
/// Children of a flex container ignore their own <c>Anchor</c> / <c>Pivot</c> /
/// <c>X</c> / <c>Y</c> — position is decided by the container. Width/Height are
/// honored as the child's intrinsic size; if 0, defaults from the layout solver
/// are used (e.g. Text → 200x32).
///
/// Algorithm — single pass per flex container, called from
/// <see cref="SuiLayoutSolver"/> after the parent's rect is known:
///
/// 1. Compute available main/cross size (parent rect minus padding).
/// 2. For each child, compute its intrinsic main/cross size.
/// 3. Sum main sizes + gaps to get used main size.
/// 4. Distribute remaining space along main axis per justify-content.
/// 5. Position each child along main; size + position along cross per align-items.
/// </summary>
public static class SuiFlexLayout
{
public readonly struct ChildLayout
{
public readonly string Id;
public readonly Rect Rect;
public ChildLayout( string id, Rect rect )
{
Id = id;
Rect = rect;
}
}
/// <summary>
/// Solve layout for one flex container's children. Returns a list of
/// (childId, rect) pairs in the same coord space as <paramref name="parentRect"/>.
/// </summary>
public static List<ChildLayout> Solve(
SuiElement parent,
Rect parentRect,
IReadOnlyDictionary<string, SuiElement> byId,
Func<SuiElement, Vector2> intrinsicSize )
{
var result = new List<ChildLayout>();
var l = parent.Layout;
if ( l == null || l.Mode != SuiLayoutMode.Flex || parent.Children == null || parent.Children.Count == 0 )
return result;
// Apply padding to get the inner content rect.
var pad = l.Padding ?? new SuiSpacing();
var inner = new Rect(
parentRect.Left + pad.Left,
parentRect.Top + pad.Top,
MathF.Max( 0, parentRect.Width - pad.Left - pad.Right ),
MathF.Max( 0, parentRect.Height - pad.Top - pad.Bottom )
);
var dir = l.FlexDirection;
var mainAxisHorizontal = dir == SuiFlexDirection.Row || dir == SuiFlexDirection.RowReverse;
var reversed = dir == SuiFlexDirection.RowReverse || dir == SuiFlexDirection.ColumnReverse;
// Resolve children with intrinsic sizes + per-child margins.
var visibleChildren = new List<SuiElement>();
var sizes = new List<Vector2>();
var margins = new List<SuiSpacing>();
foreach ( var childId in parent.Children )
{
if ( !byId.TryGetValue( childId, out var child ) ) continue;
if ( child.Style?.Visibility == SuiVisibility.Collapsed ) continue;
if ( child.Flags?.HiddenInDesigner == true ) continue;
visibleChildren.Add( child );
sizes.Add( intrinsicSize( child ) );
margins.Add( child.Layout?.Margin ?? new SuiSpacing() );
}
if ( reversed ) { visibleChildren.Reverse(); sizes.Reverse(); margins.Reverse(); }
var n = visibleChildren.Count;
if ( n == 0 ) return result;
// Compute total main-axis used size (children sizes + margins + gaps between).
var gap = MathF.Max( 0, l.Gap );
float usedMain = 0;
for ( int i = 0; i < n; i++ )
{
var s = sizes[i];
var m = margins[i];
var childMain = mainAxisHorizontal ? s.x + m.Left + m.Right : s.y + m.Top + m.Bottom;
usedMain += childMain;
}
usedMain += gap * (n - 1);
var availMain = mainAxisHorizontal ? inner.Width : inner.Height;
var freeMain = MathF.Max( 0, availMain - usedMain );
// Compute initial offset + per-gap spacing per justify-content.
float offsetMain = 0;
float perGap = gap;
switch ( l.JustifyContent )
{
case SuiJustifyContent.FlexStart:
offsetMain = 0;
break;
case SuiJustifyContent.Center:
offsetMain = freeMain * 0.5f;
break;
case SuiJustifyContent.FlexEnd:
offsetMain = freeMain;
break;
case SuiJustifyContent.SpaceBetween:
perGap = n > 1 ? gap + freeMain / (n - 1) : 0;
offsetMain = 0;
break;
case SuiJustifyContent.SpaceAround:
{
var space = n > 0 ? freeMain / n : 0;
perGap = gap + space;
offsetMain = space * 0.5f;
break;
}
case SuiJustifyContent.SpaceEvenly:
{
var space = n > 0 ? freeMain / (n + 1) : 0;
perGap = gap + space;
offsetMain = space;
break;
}
}
// Cross axis layout: each child's cross size + position depends on
// align-items. Stretch fills the inner cross size minus margins.
var availCross = mainAxisHorizontal ? inner.Height : inner.Width;
var cursor = mainAxisHorizontal ? inner.Left + offsetMain : inner.Top + offsetMain;
for ( int i = 0; i < n; i++ )
{
var child = visibleChildren[i];
var s = sizes[i];
var m = margins[i];
var marginMainStart = mainAxisHorizontal ? m.Left : m.Top;
var marginMainEnd = mainAxisHorizontal ? m.Right : m.Bottom;
var marginCrossStart= mainAxisHorizontal ? m.Top : m.Left;
var marginCrossEnd = mainAxisHorizontal ? m.Bottom : m.Right;
cursor += marginMainStart;
float crossSize, crossOffset;
switch ( l.AlignItems )
{
case SuiAlignItems.FlexEnd:
crossSize = mainAxisHorizontal ? s.y : s.x;
crossOffset = availCross - crossSize - marginCrossEnd;
break;
case SuiAlignItems.Center:
crossSize = mainAxisHorizontal ? s.y : s.x;
crossOffset = (availCross - crossSize - marginCrossStart - marginCrossEnd) * 0.5f + marginCrossStart;
break;
case SuiAlignItems.Stretch:
// Mirror CSS: align-items: stretch only stretches items whose
// cross-axis size is `auto`. Items with an explicit cross-axis
// size (Layout.Width > 0 or Layout.Height > 0 depending on
// main axis) KEEP their size — otherwise canvas would render
// 96×96 slots as 96×N where N = parent height, breaking
// parity with the actual runtime CSS render.
var hasExplicitCross = mainAxisHorizontal
? (child.Layout?.Height ?? 0f) > 0f
: (child.Layout?.Width ?? 0f) > 0f;
if ( hasExplicitCross )
{
crossSize = mainAxisHorizontal ? s.y : s.x;
crossOffset = marginCrossStart;
}
else
{
crossSize = MathF.Max( 0, availCross - marginCrossStart - marginCrossEnd );
crossOffset = marginCrossStart;
}
break;
case SuiAlignItems.Baseline: // not really baseline; treat as flex-start
case SuiAlignItems.FlexStart:
default:
crossSize = mainAxisHorizontal ? s.y : s.x;
crossOffset = marginCrossStart;
break;
}
var childMainSize = mainAxisHorizontal ? s.x : s.y;
Rect childRect;
if ( mainAxisHorizontal )
{
childRect = new Rect( cursor, inner.Top + crossOffset, childMainSize, crossSize );
}
else
{
childRect = new Rect( inner.Left + crossOffset, cursor, crossSize, childMainSize );
}
result.Add( new ChildLayout( child.Id, childRect ) );
cursor += childMainSize + marginMainEnd;
if ( i < n - 1 ) cursor += perGap;
}
return result;
}
/// <summary>
/// Grid layout for InventoryGrid / Grid. Uses parent's <c>Props.Columns</c>,
/// <c>CellWidth</c>, <c>CellHeight</c>, <c>GridGap</c> to tile children in a
/// regular grid (col-major flow). Children are positioned at fixed cell
/// sizes regardless of their own Width/Height — semantically the slot grid
/// owns the layout. Honors parent padding.
/// </summary>
public static List<ChildLayout> SolveGrid(
SuiElement parent,
Rect parentRect,
IReadOnlyDictionary<string, SuiElement> byId )
{
var result = new List<ChildLayout>();
var p = parent.Props ?? new SuiElementProps();
var cols = Math.Max( 1, p.Columns );
var cellW = p.CellWidth > 0 ? p.CellWidth : 64;
var cellH = p.CellHeight > 0 ? p.CellHeight : 64;
var gap = MathF.Max( 0, p.GridGap );
var pad = parent.Layout?.Padding ?? new SuiSpacing();
var originX = parentRect.Left + pad.Left;
var originY = parentRect.Top + pad.Top;
int idx = 0;
foreach ( var childId in parent.Children ?? new List<string>() )
{
if ( !byId.TryGetValue( childId, out var child ) ) continue;
if ( child.Style?.Visibility == SuiVisibility.Collapsed ) continue;
if ( child.Flags?.HiddenInDesigner == true ) continue;
var col = idx % cols;
var row = idx / cols;
var x = originX + col * (cellW + gap);
var y = originY + row * (cellH + gap);
result.Add( new ChildLayout( child.Id, new Rect( x, y, cellW, cellH ) ) );
idx++;
}
return result;
}
}