Editor/XGUIView.Snapping.cs
using Editor;
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.Linq;
namespace XGUI
{
public partial class XGUIView
{
private bool _isSnappedX = false;
private bool _isSnappedY = false;
private Vector2 _snapLineStartX = Vector2.Zero;
private Vector2 _snapLineEndX = Vector2.Zero;
private Vector2 _snapLineStartY = Vector2.Zero;
private Vector2 _snapLineEndY = Vector2.Zero;
private Rect? _snappingToSiblingRectX = null;
private Rect? _snappingToSiblingRectY = null;
private struct SnapCandidate
{
public float AbsOffset; // Absolute distance to snap target
public float Offset; // Signed offset needed to snap
public Vector2 LineStart; // Guide line start (parent local space)
public Vector2 LineEnd; // Guide line end (parent local space)
public Rect? TargetRect; // Rect of the target (parent local space), null for parent edge
public SnapCandidate( float dragValue, float targetValue, Vector2 lineStart, Vector2 lineEnd, Rect? targetRect )
{
Offset = targetValue - dragValue;
AbsOffset = Math.Abs( Offset );
LineStart = lineStart;
LineEnd = lineEnd;
TargetRect = targetRect;
}
}
private void DrawSnappingGuides()
{
// Draw snapping guides when dragging or resizing
if ( (isDragging && DraggingPanel != null) || (_isDraggingHandle && SelectedPanel != null) )
{
// Adjust coordinates for WindowContent position
Vector2 offset = Vector2.Zero;// WindowContent.Box.Rect.Position;
// Select the active panel based on whether we're dragging or resizing
Panel activePanel = isDragging ? DraggingPanel : SelectedPanel;
// Draw horizontal snapping guide
if ( _isSnappedY )
{
Paint.ClearPen();
// Check if this is a center snap (using the midpoint of the panel)
bool isCenterSnap = Math.Abs( _snapLineStartY.y - (activePanel.Box.Rect.Top + activePanel.Box.Rect.Height / 2 - offset.y) ) < 1f;
// Use a different color for center snaps
if ( isCenterSnap )
{
Paint.SetPen( Color.Magenta.WithAlpha( 0.8f ), 1.0f, PenStyle.Dash );
}
else
{
Paint.SetPen( Color.Yellow.WithAlpha( 0.8f ), 1.0f, PenStyle.Dash );
}
Paint.DrawLine( _snapLineStartY + offset, _snapLineEndY + offset );
}
// Draw vertical snapping guide
if ( _isSnappedX )
{
Paint.ClearPen();
// Check if this is a center snap (using the midpoint of the panel)
bool isCenterSnap = Math.Abs( _snapLineStartX.x - (activePanel.Box.Rect.Left + activePanel.Box.Rect.Width / 2 - offset.x) ) < 1f;
// Use a different color for center snaps
if ( isCenterSnap )
{
Paint.SetPen( Color.Magenta.WithAlpha( 0.8f ), 1.0f, PenStyle.Dash );
}
else
{
Paint.SetPen( Color.Yellow.WithAlpha( 0.8f ), 1.0f, PenStyle.Dash );
}
Paint.DrawLine( _snapLineStartX + offset, _snapLineEndX + offset );
}
// Draw a rectangle around the snapping target
if ( _snappingToSiblingRectX != null )
{
// No fill
Paint.ClearBrush();
Paint.SetPen( Color.Orange.WithAlpha( 0.5f ), 1.0f, PenStyle.Dash );
Paint.DrawRect( _snappingToSiblingRectX.Value );
}
if ( _snappingToSiblingRectY != null )
{
// No fill
Paint.ClearBrush();
Paint.SetPen( Color.Orange.WithAlpha( 0.5f ), 1.0f, PenStyle.Dash );
Paint.DrawRect( _snappingToSiblingRectY.Value );
}
}
}
private Vector2 ApplySnappingToPosition( Vector2 proposedPosition )
{
if ( DraggingPanel?.Parent == null )
return proposedPosition;
var parent = DraggingPanel.Parent;
float snapDistance = 10f;
float margin = 6f; // Margin for parent and sibling snapping
// --- Coordinate Setup (Parent Local Space) ---
Rect parentBounds = parent.Box.RectInner; // Inner bounds of the parent
Vector2 panelSize = DraggingPanel.Box.Rect.Size;
Rect proposedRect = new Rect( proposedPosition, panelSize );
// Dragged panel features in parent local space
float dragLeft = proposedRect.Left;
float dragRight = proposedRect.Right;
float dragTop = proposedRect.Top;
float dragBottom = proposedRect.Bottom;
float dragCenterX = proposedRect.Center.x;
float dragCenterY = proposedRect.Center.y;
var siblings = parent.Children.OfType<Panel>()
.Where( p => p != DraggingPanel )
.Select( p => p.Box.Rect ) // Sibling Rects are already in parent local space
.ToList();
// --- Candidate Generation ---
List<SnapCandidate> xCandidates = new();
List<SnapCandidate> yCandidates = new();
// Parent Snapping
float parentInnerX = parentBounds.Position.x;
float parentInnerY = parentBounds.Position.y;
float parentClientLeft = parentInnerX + margin;
float parentClientRight = parentInnerX + parentBounds.Width - margin;
float parentClientTop = parentInnerY + margin;
float parentClientBottom = parentInnerY + parentBounds.Height - margin;
float parentClientCenterX = parentInnerX + parentBounds.Width / 2f;
float parentClientCenterY = parentInnerY + parentBounds.Height / 2f;
// X-Axis (Parent) - Use adjusted targets and define lines within inner bounds
xCandidates.Add( new SnapCandidate( dragLeft, parentClientLeft, new( parentClientLeft, parentInnerY ), new( parentClientLeft, parentInnerY + parentBounds.Height ), parentBounds ) );
xCandidates.Add( new SnapCandidate( dragRight, parentClientRight, new( parentClientRight, parentInnerY ), new( parentClientRight, parentInnerY + parentBounds.Height ), parentBounds ) );
xCandidates.Add( new SnapCandidate( dragCenterX, parentClientCenterX, new( parentClientCenterX, parentInnerY ), new( parentClientCenterX, parentInnerY + parentBounds.Height ), parentBounds ) );
// Y-Axis (Parent) - Use adjusted targets and define lines within inner bounds
yCandidates.Add( new SnapCandidate( dragTop, parentClientTop, new( parentInnerX, parentClientTop ), new( parentInnerX + parentBounds.Width, parentClientTop ), parentBounds ) );
yCandidates.Add( new SnapCandidate( dragBottom, parentClientBottom, new( parentInnerX, parentClientBottom ), new( parentInnerX + parentBounds.Width, parentClientBottom ), parentBounds ) );
yCandidates.Add( new SnapCandidate( dragCenterY, parentClientCenterY, new( parentInnerX, parentClientCenterY ), new( parentInnerX + parentBounds.Width, parentClientCenterY ), parentBounds ) );
// Sibling Snapping
foreach ( var siblingRect in siblings )
{
float sLeft = siblingRect.Left;
float sRight = siblingRect.Right;
float sTop = siblingRect.Top;
float sBottom = siblingRect.Bottom;
float sCenterX = siblingRect.Center.x;
float sCenterY = siblingRect.Center.y;
// X-Axis (Siblings)
xCandidates.Add( new SnapCandidate( dragLeft, sLeft, new( sLeft, 0 ), new( sLeft, parentBounds.Height ), siblingRect ) ); // L-L
xCandidates.Add( new SnapCandidate( dragRight, sRight, new( sRight, 0 ), new( sRight, parentBounds.Height ), siblingRect ) ); // R-R
xCandidates.Add( new SnapCandidate( dragCenterX, sCenterX, new( sCenterX, 0 ), new( sCenterX, parentBounds.Height ), siblingRect ) ); // C-C
xCandidates.Add( new SnapCandidate( dragLeft, sRight, new( sRight, 0 ), new( sRight, parentBounds.Height ), siblingRect ) ); // L-R
xCandidates.Add( new SnapCandidate( dragRight, sLeft, new( sLeft, 0 ), new( sLeft, parentBounds.Height ), siblingRect ) ); // R-L
xCandidates.Add( new SnapCandidate( dragLeft, sRight + margin, new( sRight + margin, 0 ), new( sRight + margin, parentBounds.Height ), siblingRect ) ); // L-(R+m)
xCandidates.Add( new SnapCandidate( dragRight, sLeft - margin, new( sLeft - margin, 0 ), new( sLeft - margin, parentBounds.Height ), siblingRect ) ); // R-(L-m)
// Y-Axis (Siblings)
yCandidates.Add( new SnapCandidate( dragTop, sTop, new( 0, sTop ), new( parentBounds.Width, sTop ), siblingRect ) ); // T-T
yCandidates.Add( new SnapCandidate( dragBottom, sBottom, new( 0, sBottom ), new( parentBounds.Width, sBottom ), siblingRect ) ); // B-B
yCandidates.Add( new SnapCandidate( dragCenterY, sCenterY, new( 0, sCenterY ), new( parentBounds.Width, sCenterY ), siblingRect ) ); // C-C
yCandidates.Add( new SnapCandidate( dragTop, sBottom, new( 0, sBottom ), new( parentBounds.Width, sBottom ), siblingRect ) ); // T-B
yCandidates.Add( new SnapCandidate( dragBottom, sTop, new( 0, sTop ), new( parentBounds.Width, sTop ), siblingRect ) ); // B-T
yCandidates.Add( new SnapCandidate( dragTop, sBottom + margin, new( 0, sBottom + margin ), new( parentBounds.Width, sBottom + margin ), siblingRect ) ); // T-(B+m)
yCandidates.Add( new SnapCandidate( dragBottom, sTop - margin, new( 0, sTop - margin ), new( parentBounds.Width, sTop - margin ), siblingRect ) ); // B-(T-m)
}
// Filter candidates within snap distance
xCandidates = xCandidates.Where( c => c.AbsOffset < snapDistance ).ToList();
yCandidates = yCandidates.Where( c => c.AbsOffset < snapDistance ).ToList();
// --- Apply Best Snap ---
Vector2 snappedPosition = proposedPosition;
_isSnappedX = false;
_isSnappedY = false;
_snappingToSiblingRectX = null;
_snappingToSiblingRectY = null;
if ( xCandidates.Count > 0 )
{
var bestX = xCandidates.OrderBy( c => c.AbsOffset ).First();
snappedPosition.x += bestX.Offset;
_isSnappedX = true;
_snapLineStartX = bestX.LineStart;
_snapLineEndX = bestX.LineEnd;
_snappingToSiblingRectX = bestX.TargetRect; // Don't highlight parent
}
if ( yCandidates.Count > 0 )
{
var bestY = yCandidates.OrderBy( c => c.AbsOffset ).First();
snappedPosition.y += bestY.Offset;
_isSnappedY = true;
_snapLineStartY = bestY.LineStart;
_snapLineEndY = bestY.LineEnd;
_snappingToSiblingRectY = bestY.TargetRect; // Don't highlight parent
}
return snappedPosition;
}
/// <summary>
/// Applies snapping logic to resize operations.
/// Input delta is the mouse movement vector (assumed relative to parent's local space for simplicity here,
/// ensure conversion if delta originates from XGUIView's space).
/// Returns the adjusted delta based on snapping.
/// </summary>
private Vector2 ApplyResizeSnapping( Vector2 delta )
{
if ( SelectedPanel?.Parent == null || _activeHandle < 0 )
return delta;
var parent = SelectedPanel.Parent;
float snapDistance = 10f;
float margin = 6f;
float minSize = 5f; // Minimum panel size during resize
// --- Coordinate Setup (Parent Local Space) ---
Rect parentBounds = parent.Box.RectInner;
Rect originalRect = _originalRect; // Assumes _originalRect is already in parent local space
// Determine which edges are being moved
bool isLeftEdge = (_activeHandle == 0 || _activeHandle == 7 || _activeHandle == 6);
bool isRightEdge = (_activeHandle == 2 || _activeHandle == 3 || _activeHandle == 4);
bool isTopEdge = (_activeHandle == 0 || _activeHandle == 1 || _activeHandle == 2);
bool isBottomEdge = (_activeHandle == 4 || _activeHandle == 5 || _activeHandle == 6);
// Calculate the proposed rect *after* applying the raw delta
Rect proposedRect = originalRect;
if ( isLeftEdge ) { proposedRect.Left += delta.x; proposedRect.Width = Math.Max( minSize, originalRect.Width - delta.x ); }
else if ( isRightEdge ) { proposedRect.Width = Math.Max( minSize, originalRect.Width + delta.x ); }
if ( isTopEdge ) { proposedRect.Top += delta.y; proposedRect.Height = Math.Max( minSize, originalRect.Height - delta.y ); }
else if ( isBottomEdge ) { proposedRect.Height = Math.Max( minSize, originalRect.Height + delta.y ); }
// Features of the *proposed* rectangle's moving edges
float movingLeft = proposedRect.Left;
float movingRight = proposedRect.Right;
float movingTop = proposedRect.Top;
float movingBottom = proposedRect.Bottom;
// We might also want to snap the center if only one axis is moving
float movingCenterX = proposedRect.Center.x;
float movingCenterY = proposedRect.Center.y;
var siblings = parent.Children.OfType<Panel>()
.Where( p => p != SelectedPanel )
.Select( p => p.Box.Rect )
.ToList();
// --- Candidate Generation ---
List<SnapCandidate> xCandidates = new();
List<SnapCandidate> yCandidates = new();
// Parent Snapping Targets
float parentInnerX = parentBounds.Position.x;
float parentInnerY = parentBounds.Position.y;
float parentClientLeft = parentInnerX + margin;
float parentClientRight = parentInnerX + parentBounds.Width - margin;
float parentClientTop = parentInnerY + margin;
float parentClientBottom = parentInnerY + parentBounds.Height - margin;
float parentClientCenterX = parentInnerX + parentBounds.Width / 2f;
float parentClientCenterY = parentInnerY + parentBounds.Height / 2f;
// X-Axis Snapping (Parent & Siblings)
if ( isLeftEdge || isRightEdge )
{
float edgeToSnapX = isLeftEdge ? movingLeft : movingRight;
Action<SnapCandidate> addX = c => { if ( c.AbsOffset < snapDistance ) xCandidates.Add( c ); };
// Parent
addX( new SnapCandidate( edgeToSnapX, parentClientLeft, new( parentClientLeft, 0 ), new( parentClientLeft, parentBounds.Height ), parentBounds ) );
addX( new SnapCandidate( edgeToSnapX, parentClientRight, new( parentClientRight, 0 ), new( parentClientRight, parentBounds.Height ), parentBounds ) );
addX( new SnapCandidate( edgeToSnapX, parentClientCenterX, new( parentClientCenterX, 0 ), new( parentClientCenterX, parentBounds.Height ), parentBounds ) );
// Snap center only if not resizing diagonally
if ( !isTopEdge && !isBottomEdge ) addX( new SnapCandidate( movingCenterX, parentClientCenterX, new( parentClientCenterX, 0 ), new( parentClientCenterX, parentBounds.Height ), parentBounds ) );
// Siblings
foreach ( var siblingRect in siblings )
{
float sLeft = siblingRect.Left; float sRight = siblingRect.Right; float sCenterX = siblingRect.Center.x;
addX( new SnapCandidate( edgeToSnapX, sLeft, new( sLeft, 0 ), new( sLeft, parentBounds.Height ), siblingRect ) );
addX( new SnapCandidate( edgeToSnapX, sRight, new( sRight, 0 ), new( sRight, parentBounds.Height ), siblingRect ) );
addX( new SnapCandidate( edgeToSnapX, sCenterX, new( sCenterX, 0 ), new( sCenterX, parentBounds.Height ), siblingRect ) );
addX( new SnapCandidate( edgeToSnapX, sLeft - margin, new( sLeft - margin, 0 ), new( sLeft - margin, parentBounds.Height ), siblingRect ) );
addX( new SnapCandidate( edgeToSnapX, sRight + margin, new( sRight + margin, 0 ), new( sRight + margin, parentBounds.Height ), siblingRect ) );
if ( !isTopEdge && !isBottomEdge ) addX( new SnapCandidate( movingCenterX, sCenterX, new( sCenterX, 0 ), new( sCenterX, parentBounds.Height ), siblingRect ) );
}
}
// Y-Axis Snapping (Parent & Siblings)
if ( isTopEdge || isBottomEdge )
{
float edgeToSnapY = isTopEdge ? movingTop : movingBottom;
Action<SnapCandidate> addY = c => { if ( c.AbsOffset < snapDistance ) yCandidates.Add( c ); };
// Parent
addY( new SnapCandidate( edgeToSnapY, parentClientTop, new( 0, parentClientTop ), new( parentBounds.Width, parentClientTop ), parentBounds ) );
addY( new SnapCandidate( edgeToSnapY, parentClientBottom, new( 0, parentClientBottom ), new( parentBounds.Width, parentClientBottom ), parentBounds ) );
addY( new SnapCandidate( edgeToSnapY, parentClientCenterY, new( 0, parentClientCenterY ), new( parentBounds.Width, parentClientCenterY ), parentBounds ) );
// Snap center only if not resizing diagonally
if ( !isLeftEdge && !isRightEdge ) addY( new SnapCandidate( movingCenterY, parentClientCenterY, new( 0, parentClientCenterY ), new( parentBounds.Width, parentClientCenterY ), parentBounds ) );
// Siblings
foreach ( var siblingRect in siblings )
{
float sTop = siblingRect.Top; float sBottom = siblingRect.Bottom; float sCenterY = siblingRect.Center.y;
addY( new SnapCandidate( edgeToSnapY, sTop, new( 0, sTop ), new( parentBounds.Width, sTop ), siblingRect ) );
addY( new SnapCandidate( edgeToSnapY, sBottom, new( 0, sBottom ), new( parentBounds.Width, sBottom ), siblingRect ) );
addY( new SnapCandidate( edgeToSnapY, sCenterY, new( 0, sCenterY ), new( parentBounds.Width, sCenterY ), siblingRect ) );
addY( new SnapCandidate( edgeToSnapY, sTop - margin, new( 0, sTop - margin ), new( parentBounds.Width, sTop - margin ), siblingRect ) );
addY( new SnapCandidate( edgeToSnapY, sBottom + margin, new( 0, sBottom + margin ), new( parentBounds.Width, sBottom + margin ), siblingRect ) );
if ( !isLeftEdge && !isRightEdge ) addY( new SnapCandidate( movingCenterY, sCenterY, new( 0, sCenterY ), new( parentBounds.Width, sCenterY ), siblingRect ) );
}
}
// --- Apply Best Snap Adjustments ---
Vector2 adjustedDelta = delta;
_isSnappedX = false;
_isSnappedY = false;
_snappingToSiblingRectX = null;
_snappingToSiblingRectY = null;
if ( xCandidates.Count > 0 )
{
var bestX = xCandidates.OrderBy( c => c.AbsOffset ).First();
// Adjust the original delta by the offset needed to snap
adjustedDelta.x += bestX.Offset;
_isSnappedX = true;
_snapLineStartX = bestX.LineStart;
_snapLineEndX = bestX.LineEnd;
_snappingToSiblingRectX = bestX.TargetRect;
}
if ( yCandidates.Count > 0 )
{
var bestY = yCandidates.OrderBy( c => c.AbsOffset ).First();
// Adjust the original delta by the offset needed to snap
adjustedDelta.y += bestY.Offset;
_isSnappedY = true;
_snapLineStartY = bestY.LineStart;
_snapLineEndY = bestY.LineEnd;
_snappingToSiblingRectY = bestY.TargetRect;
}
return adjustedDelta;
}
}
}