using System;
using UnityEditor.ShaderGraph.Drawing.Interfaces;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.ShaderGraph.Drawing
{
class ElementResizer : Manipulator
{
bool m_IsEnabled = true;
public bool isEnabled
{
get => m_IsEnabled;
set => m_IsEnabled = value;
}
public readonly ResizableElement.Resizer direction;
public readonly VisualElement resizedElement;
public ElementResizer(VisualElement resizedElement, ResizableElement.Resizer direction)
{
this.direction = direction;
this.resizedElement = resizedElement;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
}
Vector2 m_StartMouse;
Vector2 m_StartSize;
Vector2 m_MinSize;
Vector2 m_MaxSize;
Vector2 m_StartPosition;
bool m_DragStarted = false;
void OnMouseDown(MouseDownEvent e)
{
if (!isEnabled)
return;
if (e.button == 0 && e.clickCount == 1)
{
VisualElement resizedTarget = resizedElement.parent;
if (resizedTarget != null)
{
VisualElement resizedBase = resizedTarget.parent;
if (resizedBase != null)
{
target.RegisterCallback<MouseMoveEvent>(OnMouseMove);
e.StopPropagation();
target.CaptureMouse();
m_StartMouse = resizedBase.WorldToLocal(e.mousePosition);
m_StartSize = new Vector2(resizedTarget.resolvedStyle.width, resizedTarget.resolvedStyle.height);
m_StartPosition = new Vector2(resizedTarget.resolvedStyle.left, resizedTarget.resolvedStyle.top);
bool minWidthDefined = resizedTarget.resolvedStyle.minWidth != StyleKeyword.Auto;
bool maxWidthDefined = resizedTarget.resolvedStyle.maxWidth != StyleKeyword.None;
bool minHeightDefined = resizedTarget.resolvedStyle.minHeight != StyleKeyword.Auto;
bool maxHeightDefined = resizedTarget.resolvedStyle.maxHeight != StyleKeyword.None;
m_MinSize = new Vector2(
minWidthDefined ? resizedTarget.resolvedStyle.minWidth.value : Mathf.NegativeInfinity,
minHeightDefined ? resizedTarget.resolvedStyle.minHeight.value : Mathf.NegativeInfinity);
m_MaxSize = new Vector2(
maxWidthDefined ? resizedTarget.resolvedStyle.maxWidth.value : Mathf.Infinity,
maxHeightDefined ? resizedTarget.resolvedStyle.maxHeight.value : Mathf.Infinity);
m_DragStarted = false;
}
}
}
}
void ApplyLayoutToTargetAsStyle(Rect rect)
{
var resizedTarget = resizedElement.parent;
resizedTarget.style.left = rect.x;
resizedTarget.style.top = rect.y;
resizedTarget.style.width = rect.width;
resizedTarget.style.height = rect.height;
}
static Rect CropToParent(Rect rect, Rect parent)
{
rect.xMin = Mathf.Max(rect.xMin, parent.xMin);
rect.yMin = Mathf.Max(rect.yMin, parent.yMin);
rect.xMax = Mathf.Min(rect.xMax, parent.xMax);
rect.yMax = Mathf.Min(rect.yMax, parent.yMax);
return rect;
}
static float GetSquareSize(Rect rect, ResizableElement.Resizer resizeDirection)
{
var horizontal = (resizeDirection & (ResizableElement.Resizer.Left | ResizableElement.Resizer.Right)) != 0;
var vertical = (resizeDirection & (ResizableElement.Resizer.Top | ResizableElement.Resizer.Bottom)) != 0;
if (horizontal && vertical)
{
return Mathf.Min(rect.width, rect.height);
}
return horizontal ? rect.width : rect.height;
}
Rect ApplySquareAspect(Rect element, ResizableElement.Resizer resizeDirection, Rect? keepInParent = null)
{
var newLayout = element;
var size = GetSquareSize(element, resizeDirection);
size = Mathf.Clamp(size, m_MinSize.x, m_MaxSize.x);
size = Mathf.Clamp(size, m_MinSize.y, m_MaxSize.y);
ResizeNewLayoutAndFixPosition(resizeDirection, size);
if (keepInParent is { } parentRect)
{
newLayout = CropToParent(newLayout, parentRect);
size = Mathf.Min(newLayout.width, newLayout.height);
ResizeNewLayoutAndFixPosition(resizeDirection, size);
}
return newLayout;
void ResizeNewLayoutAndFixPosition(ResizableElement.Resizer grabbedSide, float newSize)
{
newLayout.width = newSize;
newLayout.height = newSize;
var deltaWidth = element.width - newSize;
var deltaHeight = element.height - newSize;
// Anchoring rules were written with the main preview in mind, which is in the bottom-right corner
// by default.
switch (grabbedSide)
{
// Anchor to bottom-right.
case ResizableElement.Resizer.Top:
case ResizableElement.Resizer.Top | ResizableElement.Resizer.Left:
case ResizableElement.Resizer.Left:
{
newLayout.x = element.x + deltaWidth;
newLayout.y = element.y + deltaHeight;
break;
}
// Anchor to bottom-left.
case ResizableElement.Resizer.Top | ResizableElement.Resizer.Right:
{
newLayout.y = element.y + deltaHeight;
break;
}
// Anchor to top-left.
case ResizableElement.Resizer.Right:
case ResizableElement.Resizer.Bottom | ResizableElement.Resizer.Right:
case ResizableElement.Resizer.Bottom:
{
// Element is positioned by its top-left, so no adjustment is needed.
break;
}
// Anchor to top-right.
case ResizableElement.Resizer.Bottom | ResizableElement.Resizer.Left:
{
newLayout.x = element.x + deltaWidth;
break;
}
case ResizableElement.Resizer.None:
default:
break;
}
}
}
void OnMouseMove(MouseMoveEvent e)
{
if (!isEnabled)
return;
var resizedTarget = resizedElement.parent;
var resizedBase = resizedTarget.parent;
// Top left position of the parent visual element
var parentRootPosition = resizedBase.worldBound;
// Top left of the target visual element for resizing
var targetRootPosition = resizedTarget.worldBound;
var sgResizable = resizedTarget as ISGResizable;
var canResizePastParentBounds = sgResizable?.CanResizePastParentBounds() ?? false;
var keepSquareAspect = sgResizable?.KeepSquareAspect() ?? false;
var mousePos = resizedBase.WorldToLocal(e.mousePosition);
if (!m_DragStarted)
{
sgResizable?.OnStartResize();
m_DragStarted = true;
}
var newLayout = new Rect(m_StartPosition, m_StartSize);
if ((direction & ResizableElement.Resizer.Right) != 0)
{
newLayout.width = m_StartSize.x + mousePos.x - m_StartMouse.x;
var parentRightBoundary = parentRootPosition.x + resizedBase.layout.width;
// Also ensure resizing does not happen past edge of parent views boundaries if the target does not allow it
if (!canResizePastParentBounds)
{
if ((targetRootPosition.x + newLayout.width) > parentRightBoundary)
{
var targetToRightBoundaryDelta = parentRightBoundary - targetRootPosition.x;
newLayout.width = targetToRightBoundaryDelta;
}
var newLayoutLeft = targetRootPosition.x - parentRootPosition.x;
// When resizing to right, make sure to calculate and set the target elements Style.left before resizing to ensure correct resizing behavior
// If Style.left is NaNpx it results in scaling towards the left
// This is due to how the WindowDockingLayout code affects GraphSubWindows
newLayout.x = newLayoutLeft;
}
newLayout.width = Mathf.Clamp(newLayout.width, m_MinSize.x, m_MaxSize.x);
}
else if ((direction & ResizableElement.Resizer.Left) != 0)
{
float delta = mousePos.x - m_StartMouse.x;
if (m_StartSize.x - delta < m_MinSize.x)
{
delta = -m_MinSize.x + m_StartSize.x;
}
else if (m_StartSize.x - delta > m_MaxSize.x)
{
delta = -m_MaxSize.x + m_StartSize.x;
}
newLayout.width = -delta + m_StartSize.x;
var targetToLeftBoundaryDelta = delta + m_StartPosition.x;
if (!canResizePastParentBounds)
{
// This ensures that the left side of the resizing target never can get pushed past the parent boundary even if mouse is moving really fast
targetToLeftBoundaryDelta = Mathf.Clamp(targetToLeftBoundaryDelta, 2.5f, targetToLeftBoundaryDelta);
// Clamps width to max out at left edge of parent window
if (Mathf.Approximately(targetToLeftBoundaryDelta, 2.5f))
newLayout.width = (m_StartPosition.x + m_StartSize.x);
newLayout.width = Mathf.Clamp(newLayout.width, m_MinSize.x, m_MaxSize.x);
}
newLayout.x = targetToLeftBoundaryDelta;
}
if ((direction & ResizableElement.Resizer.Bottom) != 0)
{
var delta = mousePos.y - m_StartMouse.y;
newLayout.height = m_StartSize.y + delta;
var parentBottomBoundary = parentRootPosition.y + resizedBase.layout.height;
if (!canResizePastParentBounds)
{
if ((targetRootPosition.y + newLayout.height) > parentBottomBoundary)
{
var targetToBottomBoundaryDelta = parentBottomBoundary - targetRootPosition.y;
newLayout.height = targetToBottomBoundaryDelta;
}
var targetToTopBoundaryDelta = targetRootPosition.y - parentRootPosition.y;
// When resizing to bottom, make sure to calculate and set the target elements Style.top before resizing to ensure correct resizing behavior
// If Style.top is NaNpx it results in scaling towards the bottom
// This is due to how the WindowDockingLayout code affects GraphSubWindows
newLayout.y = targetToTopBoundaryDelta;
newLayout.height = Mathf.Clamp(newLayout.height, m_MinSize.y, m_MaxSize.y);
}
}
else if ((direction & ResizableElement.Resizer.Top) != 0)
{
float delta = mousePos.y - m_StartMouse.y;
if (m_StartSize.y - delta < m_MinSize.y)
{
delta = -m_MinSize.y + m_StartSize.y;
}
else if (m_StartSize.y - delta > m_MaxSize.y)
{
delta = -m_MaxSize.y + m_StartSize.y;
}
newLayout.height = -delta + m_StartSize.y;
var targetToTopBoundaryDelta = m_StartPosition.y + delta;
if (!canResizePastParentBounds)
{
// This ensures that the top of the resizing target never can get pushed past the parent boundary even if mouse is moving really fast
targetToTopBoundaryDelta = Mathf.Clamp(targetToTopBoundaryDelta, 2.5f, targetToTopBoundaryDelta);
// Clamps height to max out at top edge of parent window
if (Mathf.Approximately(targetToTopBoundaryDelta, 2.5f))
newLayout.height = (m_StartPosition.y + m_StartSize.y);
newLayout.height = Mathf.Clamp(newLayout.height, m_MinSize.y, m_MaxSize.y);
}
newLayout.y = targetToTopBoundaryDelta;
}
if (keepSquareAspect)
{
newLayout = ApplySquareAspect(
newLayout,
direction,
canResizePastParentBounds ? null : new Rect(0, 0, parentRootPosition.width, parentRootPosition.height)
);
}
ApplyLayoutToTargetAsStyle(newLayout);
e.StopPropagation();
}
void OnMouseUp(MouseUpEvent e)
{
if (!isEnabled)
return;
if (e.button == 0)
{
VisualElement resizedTarget = resizedElement.parent;
if (resizedTarget.style.width != m_StartSize.x || resizedTarget.style.height != m_StartSize.y)
{
if (resizedTarget is ISGResizable resizable)
resizable.OnResized();
}
target.UnregisterCallback<MouseMoveEvent>(OnMouseMove);
target.ReleaseMouse();
e.StopPropagation();
}
}
}
}