diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs index ee6560a84..e435e82bf 100644 --- a/Source/Editor/Surface/ResizableSurfaceNode.cs +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -1,24 +1,260 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Surface { /// - /// Visject Surface node control that cna be resized. + /// Visject Surface node control that can be resized. /// /// [HideInEditor] public class ResizableSurfaceNode : SurfaceNode { - private Float2 _startResizingSize; - private Float2 _startResizingCornerOffset; + /// + /// 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(); + } + } /// - /// Indicates whether the node is currently being resized. + /// Represents the border control used for resizing the associated element. /// - protected bool _isResizing; + public ResizeBorder ResizeBorderControl; /// /// Index of the Float2 value in the node values list to store node size. @@ -30,11 +266,6 @@ namespace FlaxEditor.Surface /// protected Float2 _sizeMin = new Float2(240, 160); - /// - /// Node resizing rectangle bounds. - /// - protected Rectangle _resizeButtonRect; - private Float2 SizeValue { get => (Float2)Values[_sizeValueIndex]; @@ -45,22 +276,31 @@ namespace FlaxEditor.Surface 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); } /// - public override bool CanSelect(ref Float2 location) + protected override void OnLocationChanged() { - return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); + ResizeBorderControl.MatchResizableNode(Size, Location); + base.OnLocationChanged(); } /// public override void OnSurfaceLoaded(SurfaceNodeActions action) { - // Reapply the curve node size + // 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); } @@ -85,104 +325,15 @@ namespace FlaxEditor.Surface { base.Draw(); - if (Surface.CanEdit) - { - var style = Style.Current; - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); - } + if (Surface.CanEdit && !Surface.IsConnecting && (ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder)) + Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); } /// - public override void OnLostFocus() + public override void OnDestroy() { - if (_isResizing) - EndResizing(); - - base.OnLostFocus(); - } - - /// - public override void OnEndMouseCapture() - { - if (_isResizing) - EndResizing(); - - base.OnEndMouseCapture(); - } - - /// - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) - { - // Start resizing - _isResizing = true; - _startResizingSize = Size; - _startResizingCornerOffset = Size - location; - StartMouseCapture(); - Cursor = CursorType.SizeNWSE; - return true; - } - - return false; - } - - /// - public override void OnMouseMove(Float2 location) - { - if (_isResizing) - { - var emptySize = CalculateNodeSize(0, 0); - var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin); - Resize(size.X, size.Y); - } - else - { - base.OnMouseMove(location); - } - } - - /// - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (button == MouseButton.Left && _isResizing) - { - EndResizing(); - return true; - } - - return base.OnMouseUp(location, button); - } - - /// - protected override void UpdateRectangles() - { - base.UpdateRectangles(); - - const float buttonMargin = Constants.NodeCloseButtonMargin; - const float buttonSize = Constants.NodeCloseButtonSize; - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); - } - - private void EndResizing() - { - Cursor = CursorType.Default; - EndMouseCapture(); - _isResizing = false; - if (_startResizingSize != Size) - { - var emptySize = CalculateNodeSize(0, 0); - SizeValue = Size - emptySize; - Surface.MarkAsEdited(false); - } + ResizeBorderControl.Parent = null; + base.OnDestroy(); } } } diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 79a285bd8..60fd3e272 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -65,6 +65,8 @@ namespace FlaxEditor.Surface EndEditOnClick = false, // We have to handle this ourselves, otherwise the textbox instantly loses focus when double-clicking the header HorizontalAlignment = TextAlignment.Center, }; + + ResizeBorderControl.IgnoreSurfaceIndex = false; } /// @@ -86,7 +88,7 @@ namespace FlaxEditor.Surface } else if (OrderValue != -1) { - IndexInParent = OrderValue; + ResizeBorderControl.IndexInParent = OrderValue; } } @@ -99,8 +101,8 @@ namespace FlaxEditor.Surface Color = ColorValue = Color.FromHSV(new Random().NextFloat(0, 360), 0.7f, 0.25f, 0.8f); if (OrderValue == -1) - OrderValue = Context.CommentCount - 1; - IndexInParent = OrderValue; + OrderValue = Context.CommentCount; + ResizeBorderControl.IndexInParent = OrderValue; } /// @@ -130,7 +132,6 @@ namespace FlaxEditor.Surface _headerRect = new Rectangle(0, 0, Width, headerSize); _closeButtonRect = new Rectangle(Width - buttonSize * 0.75f - buttonMargin, buttonMargin, buttonSize * 0.75f, buttonSize * 0.75f); _colorButtonRect = new Rectangle(_closeButtonRect.Left - buttonSize - buttonMargin, buttonMargin, buttonSize, buttonSize); - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin, buttonSize, buttonSize); _renameTextBox.Width = Width; _renameTextBox.Height = headerSize; } @@ -188,13 +189,9 @@ namespace FlaxEditor.Surface // Color button Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - // Resize button - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + // Resize + if ((ResizeBorderControl.IsResizing || ResizeBorderControl.IsMouseOverResizeBorder) && !Surface.IsConnecting) + Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.Foreground, 0.5f); } // Selection outline @@ -229,7 +226,7 @@ namespace FlaxEditor.Surface /// public override bool ContainsPoint(ref Float2 location, bool precise) { - return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location); + return _headerRect.Contains(ref location); } /// @@ -334,25 +331,25 @@ namespace FlaxEditor.Surface { cmOrder.ContextMenu.AddButton("Bring Forward", () => { - if (IndexInParent < Context.CommentCount - 1) - IndexInParent++; - OrderValue = IndexInParent; + if (ResizeBorderControl.IndexInParent < Context.CommentCount - 1) + ResizeBorderControl.IndexInParent++; + OrderValue = ResizeBorderControl.IndexInParent; }); cmOrder.ContextMenu.AddButton("Bring to Front", () => { - IndexInParent = Context.CommentCount - 1; - OrderValue = IndexInParent; + ResizeBorderControl.IndexInParent = Context.CommentCount - 1; + OrderValue = ResizeBorderControl.IndexInParent; }); cmOrder.ContextMenu.AddButton("Send Backward", () => { - if (IndexInParent > 0) - IndexInParent--; - OrderValue = IndexInParent; + if (ResizeBorderControl.IndexInParent > 0) + ResizeBorderControl.IndexInParent--; + OrderValue = ResizeBorderControl.IndexInParent; }); cmOrder.ContextMenu.AddButton("Send to Back", () => { - IndexInParent = 0; - OrderValue = IndexInParent; + ResizeBorderControl.IndexInParent = 0; + OrderValue = ResizeBorderControl.IndexInParent; }); } } diff --git a/Source/Editor/Surface/SurfaceRootControl.cs b/Source/Editor/Surface/SurfaceRootControl.cs index 15d4c43cc..c7639a27d 100644 --- a/Source/Editor/Surface/SurfaceRootControl.cs +++ b/Source/Editor/Surface/SurfaceRootControl.cs @@ -46,10 +46,10 @@ namespace FlaxEditor.Surface { var child = _children[i]; - if (child is SurfaceComment && child.Visible) + if (child is ResizableSurfaceNode.ResizeBorder border && border.ResizableNode is SurfaceComment comment2 && comment2.Visible) { - Render2D.PushTransform(ref child._cachedTransform); - child.Draw(); + Render2D.PushTransform(ref comment2._cachedTransform); + comment2.Draw(); Render2D.PopTransform(); } } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 55c643f12..f1ea1d788 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -62,6 +62,7 @@ namespace FlaxEditor.Surface private int _selectedConnectionIndex; internal int _isUpdatingBoxTypes; + internal int resizeableNodeIndexInParent = -1; /// /// True if surface supports implicit casting of the FlaxEngine.Object types into Boolean value (as simple validate check). @@ -855,10 +856,11 @@ namespace FlaxEditor.Surface int lowestCommentOrder = int.MaxValue; for (int i = 0; i < selection.Count; i++) { - if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder) - continue; - hasCommentsSelected = true; - lowestCommentOrder = selection[i].IndexInParent; + if (selection[i] is ResizableSurfaceNode node && node is SurfaceComment && node.ResizeBorderControl.IndexInParent < lowestCommentOrder) + { + hasCommentsSelected = true; + lowestCommentOrder = node.ResizeBorderControl.IndexInParent; + } } return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1); diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 560a7c1d6..b6192a198 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -80,8 +80,9 @@ namespace FlaxEditor.Surface var result = new List(); for (int i = 0; i < RootControl.Children.Count; i++) { - if (RootControl.Children[i] is SurfaceComment comment) - result.Add(comment); + var child = RootControl.Children[i]; + if (child is ResizableSurfaceNode.ResizeBorder border && border.ResizableNode is SurfaceComment comment2) + result.Add(comment2); } return result; } @@ -101,7 +102,8 @@ namespace FlaxEditor.Surface int count = 0; for (int i = 0; i < RootControl.Children.Count; i++) { - if (RootControl.Children[i] is SurfaceComment) + var child = RootControl.Children[i]; + if (child is ResizableSurfaceNode.ResizeBorder border && border.ResizableNode is SurfaceComment) count++; } return count;