Editor/Commands/SuiSetAnchorCommand.cs
using Sandbox;
using SboxUiDesigner.EditorUi.Canvas;
using SboxUiDesigner.Runtime;
namespace SboxUiDesigner.EditorUi.Commands;
/// <summary>
/// Change an element's anchor while preserving its visual rect (UMG / UI Builder
/// style). Naively assigning <c>Layout.Anchor = newValue</c> would make the
/// element jump because the same X/Y/W/H values mean different things under
/// different anchors. Instead we:
///
/// 1. Solve the document under the OLD anchor → get the element's current rect.
/// 2. Set the new anchor.
/// 3. Re-derive X/Y/W/H using <see cref="SuiLayoutSolver.RectToLayoutValues"/>
/// with the NEW anchor + the parent's rect, so the element ends up at the
/// same pixel position but with values appropriate for the new reference.
///
/// Stretch variants (Stretch / StretchHorizontal / StretchVertical) skip the
/// recompute — those modes ignore X/Y/W/H or treat them as margins, so the
/// reverse pass doesn't apply cleanly. Future polish can handle those if needed.
/// </summary>
public sealed class SuiSetAnchorCommand : ISuiCommand
{
private readonly string _elementId;
private readonly SuiAnchor _newAnchor;
private SuiAnchor _oldAnchor;
private float _oldX, _oldY, _oldW, _oldH;
public string Description => $"Set anchor to {_newAnchor}";
public SuiSetAnchorCommand( string elementId, SuiAnchor newAnchor )
{
_elementId = elementId;
_newAnchor = newAnchor;
}
public void Apply( SuiDocument doc )
{
var el = doc?.GetElement( _elementId );
if ( el?.Layout == null ) return;
_oldAnchor = el.Layout.Anchor;
_oldX = el.Layout.X;
_oldY = el.Layout.Y;
_oldW = el.Layout.Width;
_oldH = el.Layout.Height;
// Solve under the OLD anchor first to capture the current visual rect.
var solver = new SuiLayoutSolver( new Vector2( 1920, 1080 ) );
solver.Solve( doc );
if ( !solver.TryGetRect( _elementId, out var currentRect ) ||
string.IsNullOrEmpty( el.ParentId ) ||
!solver.TryGetRect( el.ParentId, out var parentRect ) )
{
el.Layout.Anchor = _newAnchor;
return;
}
// Inverse pass — derive X/Y/W/H values for the new anchor that
// reproduce the same on-screen rect. RectToLayoutValues now handles
// stretch variants too (interprets X/Y/W/H as margins).
var (x, y, w, h) = SuiLayoutSolver.RectToLayoutValues( currentRect, _newAnchor, parentRect );
el.Layout.Anchor = _newAnchor;
el.Layout.X = x;
el.Layout.Y = y;
el.Layout.Width = w;
el.Layout.Height = h;
}
public void Undo( SuiDocument doc )
{
var el = doc?.GetElement( _elementId );
if ( el?.Layout == null ) return;
el.Layout.Anchor = _oldAnchor;
el.Layout.X = _oldX;
el.Layout.Y = _oldY;
el.Layout.Width = _oldW;
el.Layout.Height = _oldH;
}
private static bool IsStretch( SuiAnchor a )
=> a == SuiAnchor.Stretch || a == SuiAnchor.StretchHorizontal || a == SuiAnchor.StretchVertical;
}