// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Surface
{
///
/// Visject Surface node control that can be resized.
///
///
[HideInEditor]
public class ResizableSurfaceNode : SurfaceNode
{
///
/// Helper class for that handles mouse interactions resizing the node itself.
///
public class ResizeBorder : ContainerControl
{
///
/// Distance to each of the 4 node edges that the cursor has to be so the user can resize along the direction of the edge.
///
private const float BorderWidth = 15f;
private readonly VisjectSurface _surface;
private Float2 _lastSurfaceMouseLoc;
private Float2 startResizingSize;
private Float2 noClampedSize;
///
/// Whether to ignore the surface index in parent when updating the cursor type. Set to false for nodes that have order like .
///
internal bool IgnoreSurfaceIndex = true;
///
/// The resizable node that this controls.
///
public readonly ResizableSurfaceNode ResizableNode;
///
/// True if the mouse is at the border of the resizable node and not further away from the border than .
///
public bool IsMouseOverResizeBorder { get; private set; }
///
/// True if is being resized.
///
public bool IsResizing { get; private set; }
///
/// The direction in which to resize the node. Should be either -1, 0 or 1 on both axes.
///
public Float2 ResizeDirection { get; private set; }
///
/// The type of cursor to show to hint to the user that they can resize the node in a given direction.
///
public CursorType CursorType
{
get
{
if ((ResizeDirection.X == 1 && ResizeDirection.Y == 0) || (ResizeDirection.X == -1 && ResizeDirection.Y == 0))
return CursorType.SizeWE;
if ((ResizeDirection.X == 0 && ResizeDirection.Y == 1) || (ResizeDirection.X == 0 && ResizeDirection.Y == -1))
return CursorType.SizeNS;
if ((ResizeDirection.X == -1 && ResizeDirection.Y == -1) || (ResizeDirection.X == 1 && ResizeDirection.Y == 1))
return CursorType.SizeNWSE;
if ((ResizeDirection.X == 1 && ResizeDirection.Y == -1) || (ResizeDirection.X == -1 && ResizeDirection.Y == 1))
return CursorType.SizeNESW;
return CursorType.Default;
}
}
///
/// Creates a new instance of .
///
/// The surface.
/// The this controls.
public ResizeBorder(VisjectSurface surface, ResizableSurfaceNode resizableNode)
{
_surface = surface;
ResizableNode = resizableNode;
}
///
/// Updates location and size to match the resizable node with the additional padding.
///
/// The node size.
/// The node location.
public void MatchResizableNode(Float2 nodeSize, Float2 nodeLocation)
{
Size = nodeSize + new Float2(BorderWidth * 2);
Location = nodeLocation - new Float2(BorderWidth);
}
private void UpdateResizeFlags(Float2 mouseLocation)
{
var borderRect = Bounds with { Location = Float2.Zero };
bool onBorder = borderRect.Contains(mouseLocation);
// Check this the way we do because some resizable nodes (like comments) have an implementation of `Control.ContainsPoint`
// that does not check for the size you would think they have based on their visual appearance
bool inNode = borderRect.MakeExpanded(-BorderWidth * 2f).Contains(mouseLocation);
Float2 rawResizeDirection = (mouseLocation - borderRect.Center);
var nodeHalfSizeNoBorder = ResizableNode.Size * 0.5f - BorderWidth;
ResizeDirection = new Float2(Mathf.Abs(rawResizeDirection.X) >= nodeHalfSizeNoBorder.X ? Mathf.Sign(rawResizeDirection.X) : 0,
Mathf.Abs(rawResizeDirection.Y) >= nodeHalfSizeNoBorder.Y ? Mathf.Sign(rawResizeDirection.Y) : 0);
IsMouseOverResizeBorder = onBorder && !inNode;
}
private Float2 GetControlDelta(Control control, Float2 start, Float2 end)
{
var pointOrigin = control.Parent ?? control;
var startPos = pointOrigin.PointFromParent(ResizableNode, start);
var endPos = pointOrigin.PointFromParent(ResizableNode, end);
return endPos - startPos;
}
private void EndResizing()
{
EndMouseCapture();
IsResizing = false;
if (startResizingSize != ResizableNode.Size)
{
var emptySize = ResizableNode.CalculateNodeSize(0, 0);
ResizableNode.SizeValue = ResizableNode.Size - emptySize;
_surface.MarkAsEdited(false);
}
}
///
public override void OnMouseMove(Float2 location)
{
if (!IsResizing)
{
UpdateResizeFlags(location);
}
else if (_surface.CanEdit)
{
var resizeAxisAbs = ResizeDirection.Absolute;
var resizeAxisPos = Float2.Clamp(ResizeDirection, Float2.Zero, Float2.One);
var resizeAxisNeg = Float2.Clamp(-ResizeDirection, Float2.Zero, Float2.One);
var currentSurfaceMouseLoc = _surface.PointFromScreen(Input.MouseScreenPosition);
var delta = currentSurfaceMouseLoc - _lastSurfaceMouseLoc;
// TODO: Snapping
delta *= resizeAxisAbs;
var moveLocation = currentSurfaceMouseLoc;
var uiControlDelta = GetControlDelta(this, _lastSurfaceMouseLoc, moveLocation);
// This ensures that the node is not resized below min size, but also keeps the resize behavior like expected (which just Max() -ing the value does not)
var emptySize = ResizableNode.CalculateNodeSize(0, 0);
var minSize = ResizableNode._sizeMin;
noClampedSize = noClampedSize + uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg;
if (noClampedSize.X < minSize.X && noClampedSize.X < ResizableNode.Size.X)
resizeAxisAbs.X = resizeAxisPos.X = resizeAxisNeg.X = 0f;
if (noClampedSize.Y < minSize.Y && noClampedSize.Y < ResizableNode.Size.Y)
resizeAxisAbs.Y = resizeAxisPos.Y = resizeAxisNeg.Y = 0f;
ResizableNode.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg;
ResizableNode.Location += uiControlDelta * resizeAxisNeg;
ResizableNode.SizeValue = ResizableNode.Size - emptySize;
ResizableNode.CalculateNodeSize(ResizableNode.Size.X, ResizableNode.Size.Y);
MatchResizableNode(ResizableNode.Size, ResizableNode.Location);
_lastSurfaceMouseLoc = currentSurfaceMouseLoc;
}
// Update the cursor shape
if ((_surface.resizeableNodeIndexInParent <= IndexInParent || IgnoreSurfaceIndex) && !_surface.IsConnecting && _surface.CanEdit)
{
if (!IgnoreSurfaceIndex)
_surface.resizeableNodeIndexInParent = IndexInParent;
if (IsMouseOverResizeBorder)
Cursor = CursorType;
else
Cursor = CursorType.Default;
}
base.OnMouseMove(location);
}
///
public override void OnMouseEnter(Float2 location)
{
Cursor = CursorType.Default;
base.OnMouseEnter(location);
}
///
public override void OnMouseLeave()
{
Cursor = CursorType.Default;
IsMouseOverResizeBorder = false;
_surface.resizeableNodeIndexInParent = -1; // Will get updated by MouseMove again to match current index
base.OnMouseLeave();
}
///
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && IsMouseOverResizeBorder && !IsResizing)
{
// Start resizing
_lastSurfaceMouseLoc = _surface.PointFromScreen(Input.MouseScreenPosition);
noClampedSize = ResizableNode.Size;
IsResizing = true;
startResizingSize = ResizableNode.Size;
StartMouseCapture();
return true;
}
return base.OnMouseDown(location, button);
}
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left && IsResizing)
{
Cursor = CursorType.Default;
EndResizing();
return true;
}
return base.OnMouseUp(location, button);
}
///
public override void OnLostFocus()
{
if (IsResizing)
EndResizing();
base.OnLostFocus();
}
///
public override void OnEndMouseCapture()
{
if (IsResizing)
EndResizing();
base.OnEndMouseCapture();
}
}
///
/// Represents the border control used for resizing the associated element.
///
public ResizeBorder ResizeBorderControl;
///
/// Index of the Float2 value in the node values list to store node size.
///
protected int _sizeValueIndex = -1;
///
/// Minimum node size.
///
protected Float2 _sizeMin = new Float2(240, 160);
private Float2 SizeValue
{
get => (Float2)Values[_sizeValueIndex];
set => SetValue(_sizeValueIndex, value, false);
}
///
public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
: base(id, context, nodeArch, groupArch)
{
ResizeBorderControl = new ResizeBorder(Surface, this)
{
Parent = Surface.SurfaceRoot,
};
Parent = ResizeBorderControl;
ResizeBorderControl.MatchResizableNode(Size, Location);
}
///
protected override void OnLocationChanged()
{
ResizeBorderControl.MatchResizableNode(Size, Location);
base.OnLocationChanged();
}
///
public override void OnSurfaceLoaded(SurfaceNodeActions action)
{
// Reapply the node size
var size = SizeValue;
if (Surface != null && Surface.GridSnappingEnabled)
size = Surface.SnapToGrid(size, true);
Resize(size.X, size.Y);
ResizeBorderControl.MatchResizableNode(Size, Location);
base.OnSurfaceLoaded(action);
}
///
public override void OnValuesChanged()
{
base.OnValuesChanged();
var size = SizeValue;
Resize(size.X, size.Y);
}
///
public override void ResizeAuto()
{
// Do nothing, we want to put full control of node size into the users hands
}
///
public override void Draw()
{
base.Draw();
if (Surface.CanEdit && !Surface.IsConnecting && (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder))
Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f);
}
///
public override void OnDestroy()
{
ResizeBorderControl.Parent = null;
base.OnDestroy();
}
}
}