diff --git a/site/Site/Pages/Documentation/Controls/Overview.razor b/site/Site/Pages/Documentation/Controls/Overview.razor index bd87fa9a1..335838ae5 100644 --- a/site/Site/Pages/Documentation/Controls/Overview.razor +++ b/site/Site/Pages/Documentation/Controls/Overview.razor @@ -3,6 +3,7 @@ @using Blazor.Diagrams.Core.Controls; @using Blazor.Diagrams.Core.PathGenerators; @using Blazor.Diagrams.Core.Positions; +@using Blazor.Diagrams.Core.Positions.Resizing; @using Blazor.Diagrams.Core.Routers; @layout DocumentationLayout @inherits DocumentationPage @@ -135,6 +136,26 @@ Diagram.Controls.AddFor(SomeModel) +

Resize Control

+ +

+ The ResizeControl adds a resizer which is a box that when dragged, can resize the node. The resizer position and movement of the node is controlled using a Resizer Provider.
+ There are four ResizerProviders, one for each corner. Custom resizing behavior can be created by implementing IResizerProvider. +

+ +

+Diagram.Controls.AddFor(SomeModel).Add(new ResizeControl(new BottomRightResizerProvider()));
+Diagram.Controls.AddFor(SomeModel).Add(new ResizeControl(new BottomLeftResizerProvider()));
+Diagram.Controls.AddFor(SomeModel).Add(new ResizeControl(new TopRightResizerProvider()));
+Diagram.Controls.AddFor(SomeModel).Add(new ResizeControl(new TopLeftResizerProvider()));
+
+ +
+ + + +
+ @@ -143,6 +164,7 @@ Diagram.Controls.AddFor(SomeModel) private BlazorDiagram _rDiagram = new(); private BlazorDiagram _ahDiagram = new(); private BlazorDiagram _dnlDiagram = new(); + private BlazorDiagram _resizerDiagram = new(); protected override void OnInitialized() { @@ -198,5 +220,18 @@ Diagram.Controls.AddFor(SomeModel) _dnlDiagram.Controls.AddFor(dnlNode2).Add(new DragNewLinkControl(0, 0.5, offsetX: -20)); _dnlDiagram.SelectModel(dnlNode1, false); _dnlDiagram.SelectModel(dnlNode2, false); + + // Resize Control + var resizeNode1 = _resizerDiagram.Nodes.Add(new NodeModel(new Point(100, 100))); + var resizeNode2 = _resizerDiagram.Nodes.Add(new NodeModel(new Point(500, 150))); + resizeNode1.Title = "Title"; + resizeNode2.Title = "Title"; + _resizerDiagram.Controls.AddFor(resizeNode1, ControlsType.OnSelection).Add(new ResizeControl(new BottomRightResizerProvider())); + _resizerDiagram.Controls.AddFor(resizeNode1, ControlsType.OnSelection).Add(new ResizeControl(new BottomLeftResizerProvider())); + _resizerDiagram.Controls.AddFor(resizeNode1, ControlsType.OnSelection).Add(new ResizeControl(new TopRightResizerProvider())); + _resizerDiagram.Controls.AddFor(resizeNode1, ControlsType.OnSelection).Add(new ResizeControl(new TopLeftResizerProvider())); + _resizerDiagram.Controls.AddFor(resizeNode2, ControlsType.OnSelection).Add(new ResizeControl(new BottomRightResizerProvider())); + _resizerDiagram.SelectModel(resizeNode1, false); + _resizerDiagram.SelectModel(resizeNode2, false); } } \ No newline at end of file diff --git a/src/Blazor.Diagrams.Core/Controls/Default/ResizeControl.cs b/src/Blazor.Diagrams.Core/Controls/Default/ResizeControl.cs new file mode 100644 index 000000000..4517f673d --- /dev/null +++ b/src/Blazor.Diagrams.Core/Controls/Default/ResizeControl.cs @@ -0,0 +1,38 @@ +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models.Base; +using Blazor.Diagrams.Core.Positions.Resizing; +using System.Threading.Tasks; + +namespace Blazor.Diagrams.Core.Controls.Default +{ + public class ResizeControl : ExecutableControl + { + private readonly IResizerProvider _resizeProvider; + + public ResizeControl(IResizerProvider resizeProvider) + { + _resizeProvider = resizeProvider; + } + + public override Point? GetPosition(Model model) => _resizeProvider.GetPosition(model); + + public string? Class => _resizeProvider.Class; + + public override ValueTask OnPointerDown(Diagram diagram, Model model, PointerEventArgs e) + { + _resizeProvider.OnResizeStart(diagram, model, e); + diagram.PointerMove += _resizeProvider.OnPointerMove; + diagram.PointerUp += _resizeProvider.OnResizeEnd; + diagram.PointerUp += (_, _) => OnResizeEnd(diagram); + + return ValueTask.CompletedTask; + } + + void OnResizeEnd(Diagram diagram) + { + diagram.PointerMove -= _resizeProvider.OnPointerMove; + diagram.PointerUp -= _resizeProvider.OnResizeEnd; + } + } +} diff --git a/src/Blazor.Diagrams.Core/Models/GroupModel.cs b/src/Blazor.Diagrams.Core/Models/GroupModel.cs index 041d872ec..4a450be1b 100644 --- a/src/Blazor.Diagrams.Core/Models/GroupModel.cs +++ b/src/Blazor.Diagrams.Core/Models/GroupModel.cs @@ -27,13 +27,10 @@ public void AddChild(NodeModel child) { _children.Add(child); child.Group = this; - child.SizeChanged += OnNodeChanged; + child.SizeChanging += OnNodeChanged; child.Moving += OnNodeChanged; - if (UpdateDimensions()) - { - Refresh(); - } + UpdateDimensions(); } public void RemoveChild(NodeModel child) @@ -42,14 +39,10 @@ public void RemoveChild(NodeModel child) return; child.Group = null; - child.SizeChanged -= OnNodeChanged; + child.SizeChanging -= OnNodeChanged; child.Moving -= OnNodeChanged; - if (UpdateDimensions()) - { - Refresh(); - RefreshLinks(); - } + UpdateDimensions(); } public override void SetPosition(double x, double y) @@ -83,7 +76,7 @@ public void Ungroup() foreach (var child in Children) { child.Group = null; - child.SizeChanged -= OnNodeChanged; + child.SizeChanging -= OnNodeChanged; child.Moving -= OnNodeChanged; } @@ -96,7 +89,7 @@ private void Initialize(IEnumerable children) { _children.Add(child); child.Group = this; - child.SizeChanged += OnNodeChanged; + child.SizeChanging += OnNodeChanged; child.Moving += OnNodeChanged; } @@ -105,10 +98,7 @@ private void Initialize(IEnumerable children) private void OnNodeChanged(NodeModel node) { - if (UpdateDimensions()) - { - Refresh(); - } + UpdateDimensions(); } private bool UpdateDimensions() @@ -128,7 +118,7 @@ private bool UpdateDimensions() TriggerMoving(); } - Size = new Size(bounds.Width + Padding * 2, bounds.Height + Padding * 2); + SetSize(bounds.Width + Padding * 2, bounds.Height + Padding * 2); return true; } } diff --git a/src/Blazor.Diagrams.Core/Models/NodeModel.cs b/src/Blazor.Diagrams.Core/Models/NodeModel.cs index 053c5b3d8..610a5aac3 100644 --- a/src/Blazor.Diagrams.Core/Models/NodeModel.cs +++ b/src/Blazor.Diagrams.Core/Models/NodeModel.cs @@ -11,7 +11,9 @@ public class NodeModel : MovableModel, IHasBounds, IHasShape, ILinkable private readonly List _ports = new(); private readonly List _links = new(); private Size? _size; + public Size MinimumDimensions { get; set; } = new Size(0, 0); + public event Action? SizeChanging; public event Action? SizeChanged; public event Action? Moving; @@ -28,11 +30,7 @@ public Size? Size get => _size; set { - if (value?.Equals(_size) == true) - return; - _size = value; - SizeChanged?.Invoke(this); } } public bool ControlledSize { get; init; } @@ -103,6 +101,23 @@ public override void SetPosition(double x, double y) Moving?.Invoke(this); } + public void SetSize(double width, double height) + { + var newSize = new Size(width, height); + if (newSize.Equals(_size) == true) + return; + + Size = newSize; + Refresh(); + RefreshLinks(); + SizeChanging?.Invoke(this); + } + + public void TriggerSizeChanged() + { + SizeChanged?.Invoke(this); + } + public virtual void UpdatePositionSilently(double deltaX, double deltaY) { base.SetPosition(Position.X + deltaX, Position.Y + deltaY); diff --git a/src/Blazor.Diagrams.Core/Positions/Resizing/BottomLeftResizerProvider.cs b/src/Blazor.Diagrams.Core/Positions/Resizing/BottomLeftResizerProvider.cs new file mode 100644 index 000000000..c4e576c92 --- /dev/null +++ b/src/Blazor.Diagrams.Core/Positions/Resizing/BottomLeftResizerProvider.cs @@ -0,0 +1,75 @@ +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace Blazor.Diagrams.Core.Positions.Resizing +{ + public class BottomLeftResizerProvider : IResizerProvider + { + public string? Class => "bottomleft"; + + private Size _originalSize = null!; + private Point _originalPosition = null!; + private Point _originalMousePosition = null!; + private NodeModel _nodeModel = null!; + + public Point? GetPosition(Model model) + { + if (model is NodeModel nodeModel && nodeModel.Size is not null) + { + return new Point(nodeModel.Position.X - 5, nodeModel.Position.Y + nodeModel.Size.Height + 5); + } + return null; + } + + public void OnResizeStart(Diagram diagram, Model model, PointerEventArgs eventArgs) + { + if (model is NodeModel nodeModel) + { + _originalPosition = new Point(nodeModel.Position.X, nodeModel.Position.Y); + _originalMousePosition = new Point(eventArgs.ClientX, eventArgs.ClientY); + _originalSize = nodeModel.Size!; + _nodeModel = nodeModel; + } + } + + public void OnPointerMove(Model? model, PointerEventArgs args) + { + if (_nodeModel is null) + { + return; + } + + var height = _originalSize.Height + (args.ClientY - _originalMousePosition.Y); + var width = _originalSize.Width - (args.ClientX - _originalMousePosition.X); + + var positionX = _originalPosition.X + (args.ClientX - _originalMousePosition.X); + var positionY = _originalPosition.Y; + + if (width < _nodeModel.MinimumDimensions.Width) + { + width = _nodeModel.MinimumDimensions.Width; + positionX = _nodeModel.Position.X; + } + if (height < _nodeModel.MinimumDimensions.Height) + { + height = _nodeModel.MinimumDimensions.Height; + positionY = _nodeModel.Position.Y; + } + + _nodeModel.SetPosition(positionX, positionY); + _nodeModel.SetSize(width, height); + } + + public void OnResizeEnd(Model? model, PointerEventArgs args) + { + _nodeModel?.TriggerSizeChanged(); + _originalSize = null!; + _originalPosition = null!; + _originalMousePosition = null!; + _nodeModel = null!; + } + + } +} diff --git a/src/Blazor.Diagrams.Core/Positions/Resizing/BottomRightResizerProvider.cs b/src/Blazor.Diagrams.Core/Positions/Resizing/BottomRightResizerProvider.cs new file mode 100644 index 000000000..f55e26d71 --- /dev/null +++ b/src/Blazor.Diagrams.Core/Positions/Resizing/BottomRightResizerProvider.cs @@ -0,0 +1,66 @@ +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace Blazor.Diagrams.Core.Positions.Resizing +{ + public class BottomRightResizerProvider : IResizerProvider + { + public string? Class => "bottomright"; + + private Size _originalSize = null!; + private Point _originalMousePosition = null!; + private NodeModel _nodeModel = null!; + + public Point? GetPosition(Model model) + { + if (model is NodeModel nodeModel && nodeModel.Size is not null) + { + return new Point(nodeModel.Position.X + nodeModel.Size.Width + 5, nodeModel.Position.Y + nodeModel.Size.Height + 5); + } + return null; + } + + public void OnResizeStart(Diagram diagram, Model model, PointerEventArgs eventArgs) + { + if (model is NodeModel nodeModel) + { + _originalMousePosition = new Point(eventArgs.ClientX, eventArgs.ClientY); + _originalSize = nodeModel.Size!; + _nodeModel = nodeModel; + } + } + + public void OnPointerMove(Model? model, PointerEventArgs args) + { + if (_nodeModel is null) + { + return; + } + + var height = _originalSize.Height + (args.ClientY - _originalMousePosition.Y); + var width = _originalSize.Width + (args.ClientX - _originalMousePosition.X); + + if (width < _nodeModel.MinimumDimensions.Width) + { + width = _nodeModel.MinimumDimensions.Width; + } + if (height < _nodeModel.MinimumDimensions.Height) + { + height = _nodeModel.MinimumDimensions.Height; + } + + _nodeModel.SetSize(width, height); + } + + public void OnResizeEnd(Model? model, PointerEventArgs args) + { + _nodeModel?.TriggerSizeChanged(); + _originalSize = null!; + _originalMousePosition = null!; + _nodeModel = null!; + } + + } +} diff --git a/src/Blazor.Diagrams.Core/Positions/Resizing/IResizerProvider.cs b/src/Blazor.Diagrams.Core/Positions/Resizing/IResizerProvider.cs new file mode 100644 index 000000000..65a2ecdde --- /dev/null +++ b/src/Blazor.Diagrams.Core/Positions/Resizing/IResizerProvider.cs @@ -0,0 +1,14 @@ +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace Blazor.Diagrams.Core.Positions.Resizing +{ + public interface IResizerProvider : IPositionProvider + { + public string? Class { get; } + public void OnResizeStart(Diagram diagram, Model model, PointerEventArgs eventArgs); + public void OnPointerMove(Model? model, PointerEventArgs args); + public void OnResizeEnd(Model? model, PointerEventArgs args); + } +} diff --git a/src/Blazor.Diagrams.Core/Positions/Resizing/TopLeftResizerProvider.cs b/src/Blazor.Diagrams.Core/Positions/Resizing/TopLeftResizerProvider.cs new file mode 100644 index 000000000..7c3afe5ce --- /dev/null +++ b/src/Blazor.Diagrams.Core/Positions/Resizing/TopLeftResizerProvider.cs @@ -0,0 +1,75 @@ +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace Blazor.Diagrams.Core.Positions.Resizing +{ + public class TopLeftResizerProvider : IResizerProvider + { + public string? Class => "topleft"; + + private Size _originalSize = null!; + private Point _originalPosition = null!; + private Point _originalMousePosition = null!; + private NodeModel _nodeModel = null!; + + public Point? GetPosition(Model model) + { + if (model is NodeModel nodeModel && nodeModel.Size is not null) + { + return new Point(nodeModel.Position.X - 5, nodeModel.Position.Y - 5); + } + return null; + } + + public void OnResizeStart(Diagram diagram, Model model, PointerEventArgs eventArgs) + { + if (model is NodeModel nodeModel) + { + _originalPosition = new Point(nodeModel.Position.X, nodeModel.Position.Y); + _originalMousePosition = new Point(eventArgs.ClientX, eventArgs.ClientY); + _originalSize = nodeModel.Size!; + _nodeModel = nodeModel; + } + } + + public void OnPointerMove(Model? model, PointerEventArgs args) + { + if (_nodeModel is null) + { + return; + } + + var height = _originalSize.Height - (args.ClientY - _originalMousePosition.Y); + var width = _originalSize.Width - (args.ClientX - _originalMousePosition.X); + + var positionX = _originalPosition.X + (args.ClientX - _originalMousePosition.X); + var positionY = _originalPosition.Y + (args.ClientY - _originalMousePosition.Y); + + if (width < _nodeModel.MinimumDimensions.Width) + { + width = _nodeModel.MinimumDimensions.Width; + positionX = _nodeModel.Position.X; + } + if (height < _nodeModel.MinimumDimensions.Height) + { + height = _nodeModel.MinimumDimensions.Height; + positionY = _nodeModel.Position.Y; + } + + _nodeModel.SetPosition(positionX, positionY); + _nodeModel.SetSize(width, height); + } + + public void OnResizeEnd(Model? model, PointerEventArgs args) + { + _nodeModel?.TriggerSizeChanged(); + _originalSize = null!; + _originalPosition = null!; + _originalMousePosition = null!; + _nodeModel = null!; + } + + } +} diff --git a/src/Blazor.Diagrams.Core/Positions/Resizing/TopRightResizerProvider.cs b/src/Blazor.Diagrams.Core/Positions/Resizing/TopRightResizerProvider.cs new file mode 100644 index 000000000..e9ea4e14a --- /dev/null +++ b/src/Blazor.Diagrams.Core/Positions/Resizing/TopRightResizerProvider.cs @@ -0,0 +1,74 @@ +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Models.Base; + +namespace Blazor.Diagrams.Core.Positions.Resizing +{ + public class TopRightResizerProvider : IResizerProvider + { + public string? Class => "topright"; + + private Size _originalSize = null!; + private Point _originalPosition = null!; + private Point _originalMousePosition = null!; + private NodeModel _nodeModel = null!; + + public Point? GetPosition(Model model) + { + if (model is NodeModel nodeModel && nodeModel.Size is not null) + { + return new Point(nodeModel.Position.X + nodeModel.Size.Width + 5, nodeModel.Position.Y - 5); + } + return null; + } + + public void OnResizeStart(Diagram diagram, Model model, PointerEventArgs eventArgs) + { + if (model is NodeModel nodeModel) + { + _originalPosition = new Point(nodeModel.Position.X, nodeModel.Position.Y); + _originalMousePosition = new Point(eventArgs.ClientX, eventArgs.ClientY); + _originalSize = nodeModel.Size!; + _nodeModel = nodeModel; + } + } + + public void OnPointerMove(Model? model, PointerEventArgs args) + { + if (_nodeModel is null) + { + return; + } + var height = _originalSize.Height - (args.ClientY - _originalMousePosition.Y); + var width = _originalSize.Width + (args.ClientX - _originalMousePosition.X); + + var positionX = _originalPosition.X; + var positionY = _originalPosition.Y + (args.ClientY - _originalMousePosition.Y); + + if (width < _nodeModel.MinimumDimensions.Width) + { + width = _nodeModel.MinimumDimensions.Width; + positionX = _nodeModel.Position.X; + } + if (height < _nodeModel.MinimumDimensions.Height) + { + height = _nodeModel.MinimumDimensions.Height; + positionY = _nodeModel.Position.Y; + } + + _nodeModel.SetPosition(positionX, positionY); + _nodeModel.SetSize(width, height); + } + + public void OnResizeEnd(Model? model, PointerEventArgs args) + { + _nodeModel?.TriggerSizeChanged(); + _originalSize = null!; + _originalPosition = null!; + _originalMousePosition = null!; + _nodeModel = null!; + } + + } +} diff --git a/src/Blazor.Diagrams/BlazorDiagram.cs b/src/Blazor.Diagrams/BlazorDiagram.cs index ab61d98a8..b18eab9dc 100644 --- a/src/Blazor.Diagrams/BlazorDiagram.cs +++ b/src/Blazor.Diagrams/BlazorDiagram.cs @@ -19,7 +19,8 @@ public BlazorDiagram(BlazorDiagramOptions? options = null) [typeof(RemoveControl)] = typeof(RemoveControlWidget), [typeof(BoundaryControl)] = typeof(BoundaryControlWidget), [typeof(DragNewLinkControl)] = typeof(DragNewLinkControlWidget), - [typeof(ArrowHeadControl)] = typeof(ArrowHeadControlWidget) + [typeof(ArrowHeadControl)] = typeof(ArrowHeadControlWidget), + [typeof(ResizeControl)] = typeof(ResizeControlWidget) }; Options = options ?? new BlazorDiagramOptions(); diff --git a/src/Blazor.Diagrams/Components/Controls/ResizeControlWidget.razor b/src/Blazor.Diagrams/Components/Controls/ResizeControlWidget.razor new file mode 100644 index 000000000..cfd15bd7b --- /dev/null +++ b/src/Blazor.Diagrams/Components/Controls/ResizeControlWidget.razor @@ -0,0 +1,10 @@ +
+ +@code +{ + [Parameter] + public ResizeControl Control { get; set; } = null!; + + [Parameter] + public Model Model { get; set; } = null!; +} diff --git a/src/Blazor.Diagrams/Components/NodeWidget.razor b/src/Blazor.Diagrams/Components/NodeWidget.razor index 37ff3da8d..726e51d2c 100644 --- a/src/Blazor.Diagrams/Components/NodeWidget.razor +++ b/src/Blazor.Diagrams/Components/NodeWidget.razor @@ -1,4 +1,4 @@ -
+
@(Node.Title ?? "Title") @foreach (var port in Node.Ports) { @@ -11,4 +11,12 @@ [Parameter] public NodeModel Node { get; set; } = null!; + private string GenerateStyle() + { + if (Node.Size is not null) + { + return $"width: {Node.Size.Width}px; height: {Node.Size.Height}px"; + } + return string.Empty; + } } \ No newline at end of file diff --git a/src/Blazor.Diagrams/wwwroot/default.styles.css b/src/Blazor.Diagrams/wwwroot/default.styles.css index b095bbe4e..2b355029a 100644 --- a/src/Blazor.Diagrams/wwwroot/default.styles.css +++ b/src/Blazor.Diagrams/wwwroot/default.styles.css @@ -127,4 +127,28 @@ g.diagram-group.default.selected > rect { transform: translate(-50%, -50%); } +.default-node-resizer { + width: 5px; + height: 5px; + background-color: #f5f5f5; + border: 1px solid #6e9fd4; + position: absolute; +} + +.default-node-resizer.bottomright { + cursor: nwse-resize; +} + +.default-node-resizer.topright { + cursor: nesw-resize; +} + +.default-node-resizer.bottomleft { + cursor: nesw-resize; +} + +.default-node-resizer.topleft { + cursor: nwse-resize; +} + /*# sourceMappingURL=wwwroot\default.styles.css.map */ diff --git a/src/Blazor.Diagrams/wwwroot/default.styles.min.css b/src/Blazor.Diagrams/wwwroot/default.styles.min.css index c44adedc3..e794e823e 100644 --- a/src/Blazor.Diagrams/wwwroot/default.styles.min.css +++ b/src/Blazor.Diagrams/wwwroot/default.styles.min.css @@ -1 +1 @@ -.default-node{width:100px;height:80px;border-radius:10px;background-color:#f5f5f5;border:1px solid #e8e8e8;-webkit-box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;}.default-node.selected{border:1px solid #6e9fd4;}.default-node.selected .diagram-port{border:1px solid #6e9fd4;}.default-node .diagram-port,.default.diagram-group .diagram-port{width:20px;height:20px;margin:-10px;border-radius:50%;background-color:#f5f5f5;border:1px solid #d4d4d4;cursor:pointer;position:absolute;}.default-node .diagram-port:hover,.default-node .diagram-port.has-links,.default.diagram-group .diagram-port.has-links{background-color:#000;}.default-node .diagram-port.bottom,.default.diagram-group .diagram-port.bottom{bottom:0;left:50%;}.default-node .diagram-port.bottomleft,.default.diagram-group .diagram-port.bottomleft{bottom:0;left:0;}.default-node .diagram-port.bottomright,.default.diagram-group .diagram-port.bottomright{bottom:0;right:0;}.default-node .diagram-port.top,.default.diagram-group .diagram-port.top{top:0;left:50%;}.default-node .diagram-port.topleft,.default.diagram-group .diagram-port.topleft{top:0;left:0;}.default-node .diagram-port.topright,.default.diagram-group .diagram-port.topright{top:0;right:0;}.default-node .diagram-port.left,.default.diagram-group .diagram-port.left{left:0;top:50%;}.default-node .diagram-port.right,.default.diagram-group .diagram-port.right{right:0;top:50%;}.diagram-navigator.default{position:absolute;bottom:10px;right:10px;border:3px solid #9ba8b0;border-radius:15px;padding:20px;background-color:#fff;}div.diagram-group.default{outline:2px solid #000;background:#c6c6c6;}div.diagram-group.default.selected{outline:2px solid #6e9fd4;}g.diagram-group.default rect{outline:2px solid #000;fill:#c6c632;}g.diagram-group.default.selected>rect{outline:2px solid #008000;}.diagram-link div.default-link-label{display:inline-block;color:#fff;background-color:#6e9fd4;border-radius:.25rem;padding:.25rem;text-align:center;font-size:.875rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;min-width:3rem;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);} \ No newline at end of file +.default-node{width:100px;height:80px;border-radius:10px;background-color:#f5f5f5;border:1px solid #e8e8e8;-webkit-box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;}.default-node.selected{border:1px solid #6e9fd4;}.default-node.selected .diagram-port{border:1px solid #6e9fd4;}.default-node .diagram-port,.default.diagram-group .diagram-port{width:20px;height:20px;margin:-10px;border-radius:50%;background-color:#f5f5f5;border:1px solid #d4d4d4;cursor:pointer;position:absolute;}.default-node .diagram-port:hover,.default-node .diagram-port.has-links,.default.diagram-group .diagram-port.has-links{background-color:#000;}.default-node .diagram-port.bottom,.default.diagram-group .diagram-port.bottom{bottom:0;left:50%;}.default-node .diagram-port.bottomleft,.default.diagram-group .diagram-port.bottomleft{bottom:0;left:0;}.default-node .diagram-port.bottomright,.default.diagram-group .diagram-port.bottomright{bottom:0;right:0;}.default-node .diagram-port.top,.default.diagram-group .diagram-port.top{top:0;left:50%;}.default-node .diagram-port.topleft,.default.diagram-group .diagram-port.topleft{top:0;left:0;}.default-node .diagram-port.topright,.default.diagram-group .diagram-port.topright{top:0;right:0;}.default-node .diagram-port.left,.default.diagram-group .diagram-port.left{left:0;top:50%;}.default-node .diagram-port.right,.default.diagram-group .diagram-port.right{right:0;top:50%;}.diagram-navigator.default{position:absolute;bottom:10px;right:10px;border:3px solid #9ba8b0;border-radius:15px;padding:20px;background-color:#fff;}div.diagram-group.default{outline:2px solid #000;background:#c6c6c6;}div.diagram-group.default.selected{outline:2px solid #6e9fd4;}g.diagram-group.default rect{outline:2px solid #000;fill:#c6c632;}g.diagram-group.default.selected>rect{outline:2px solid #008000;}.diagram-link div.default-link-label{display:inline-block;color:#fff;background-color:#6e9fd4;border-radius:.25rem;padding:.25rem;text-align:center;font-size:.875rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;min-width:3rem;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);}.default-node-resizer{width:5px;height:5px;background-color:#f5f5f5;border:1px solid #6e9fd4;position:absolute;}.default-node-resizer.bottomright{cursor:nwse-resize;}.default-node-resizer.topright{cursor:nesw-resize;}.default-node-resizer.bottomleft{cursor:nesw-resize;}.default-node-resizer.topleft{cursor:nwse-resize;} \ No newline at end of file diff --git a/src/Blazor.Diagrams/wwwroot/default.styles.min.css.gz b/src/Blazor.Diagrams/wwwroot/default.styles.min.css.gz index 905f20cd3..3c43a8a10 100644 Binary files a/src/Blazor.Diagrams/wwwroot/default.styles.min.css.gz and b/src/Blazor.Diagrams/wwwroot/default.styles.min.css.gz differ diff --git a/src/Blazor.Diagrams/wwwroot/style.min.css.gz b/src/Blazor.Diagrams/wwwroot/style.min.css.gz index e72cb7d8a..4a312feec 100644 Binary files a/src/Blazor.Diagrams/wwwroot/style.min.css.gz and b/src/Blazor.Diagrams/wwwroot/style.min.css.gz differ diff --git a/tests/Blazor.Diagrams.Core.Tests/Controls/ResizeControlTests.cs b/tests/Blazor.Diagrams.Core.Tests/Controls/ResizeControlTests.cs new file mode 100644 index 000000000..dcd9bab14 --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Controls/ResizeControlTests.cs @@ -0,0 +1,75 @@ +using Blazor.Diagrams.Core.Controls.Default; +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Models.Base; +using Blazor.Diagrams.Core.Positions.Resizing; +using Moq; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Controls +{ + public class ResizeControlTests + { + [Fact] + public void GetPosition_ShouldUseResizeProviderGetPosition() + { + var resizeProvider = new Mock(); + var control = new ResizeControl(resizeProvider.Object); + var model = new Mock(); + + control.GetPosition(model.Object); + + resizeProvider.Verify(m => m.GetPosition(model.Object), Times.Once); + } + + [Fact] + public void OnPointerDown_ShouldInvokeResizeStart() + { + var resizeProvider = new Mock(); + var control = new ResizeControl(resizeProvider.Object); + var diagram = Mock.Of(); + var model = Mock.Of(); + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 0, 0, 0, 0, 0, 0, string.Empty, true); + + control.OnPointerDown(diagram, model, eventArgs); + + resizeProvider.Verify(m => m.OnResizeStart(diagram, model, eventArgs), Times.Once); + } + + [Fact] + public void OnPointerDown_ShouldAddEventHandlers() + { + var resizeProvider = new Mock(); + var control = new ResizeControl(resizeProvider.Object); + var diagram = new TestDiagram(); + var model = Mock.Of(); + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 0, 0, 0, 0, 0, 0, string.Empty, true); + + control.OnPointerDown(diagram, model, eventArgs); + + diagram.TriggerPointerMove(model, eventArgs); + resizeProvider.Verify(m => m.OnPointerMove(model, eventArgs), Times.Once); + + diagram.TriggerPointerUp(model, eventArgs); + resizeProvider.Verify(m => m.OnResizeEnd(model, eventArgs), Times.Once); + } + + [Fact] + public void OnPointerUp_ShouldRemoveEventHandlers() + { + var resizeProvider = new Mock(); + var control = new ResizeControl(resizeProvider.Object); + var diagram = new TestDiagram(); + var model = Mock.Of(); + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 0, 0, 0, 0, 0, 0, string.Empty, true); + + control.OnPointerDown(diagram, model, eventArgs); + diagram.TriggerPointerUp(model, eventArgs); + + diagram.TriggerPointerMove(model, eventArgs); + resizeProvider.Verify(m => m.OnPointerMove(model, eventArgs), Times.Never); + + diagram.TriggerPointerUp(model, eventArgs); + resizeProvider.Verify(m => m.OnResizeEnd(model, eventArgs), Times.Once); + } + } +} diff --git a/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/BottomLeftResizerProviderTests.cs b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/BottomLeftResizerProviderTests.cs new file mode 100644 index 000000000..79ef6c17b --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/BottomLeftResizerProviderTests.cs @@ -0,0 +1,82 @@ +using Blazor.Diagrams.Core.Controls.Default; +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Positions.Resizing; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Positions.Resizing +{ + public class BottomLeftResizerProviderTests + { + [Fact] + public void DragResizer_ShouldResizeNode() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new BottomLeftResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(10, 15, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(10); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(90); + node.Size.Height.Should().Be(215); + } + + [Fact] + public void DragResizer_SmallerThanMinSize_SetsNodeToMinSize() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new BottomLeftResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(99, -199, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + eventArgs = new PointerEventArgs(300, -300, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(99); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(0); + node.Size.Height.Should().Be(0); + } + } +} diff --git a/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/BottomRightResizerProviderTests.cs b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/BottomRightResizerProviderTests.cs new file mode 100644 index 000000000..20ab27770 --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/BottomRightResizerProviderTests.cs @@ -0,0 +1,80 @@ +using Blazor.Diagrams.Core.Controls.Default; +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Positions.Resizing; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Positions.Resizing +{ + public class BottomRightResizerProviderTests + { + [Fact] + public void DragResizer_ShouldResizeNode() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new BottomRightResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(10, 15, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(110); + node.Size.Height.Should().Be(215); + } + + [Fact] + public void DragResizer_SmallerThanMinSize_SetsNodeToMinSize() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new BottomRightResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(-300, -300, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(0); + node.Size.Height.Should().Be(0); + } + } +} diff --git a/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/TopLeftResizerProviderTests.cs b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/TopLeftResizerProviderTests.cs new file mode 100644 index 000000000..1cc715d59 --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/TopLeftResizerProviderTests.cs @@ -0,0 +1,82 @@ +using Blazor.Diagrams.Core.Controls.Default; +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Positions.Resizing; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Positions.Resizing +{ + public class TopLeftResizerProviderTests + { + [Fact] + public void DragResizer_ShouldResizeNode() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new TopLeftResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(10, 15, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(10); + node.Position.Y.Should().Be(15); + node.Size.Width.Should().Be(90); + node.Size.Height.Should().Be(185); + } + + [Fact] + public void DragResizer_SmallerThanMinSize_SetsNodeToMinSize() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new TopLeftResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(99, 199, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + eventArgs = new PointerEventArgs(300, 300, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(99); + node.Position.Y.Should().Be(199); + node.Size.Width.Should().Be(0); + node.Size.Height.Should().Be(0); + } + } +} diff --git a/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/TopRightResizerProviderTests.cs b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/TopRightResizerProviderTests.cs new file mode 100644 index 000000000..a138d524f --- /dev/null +++ b/tests/Blazor.Diagrams.Core.Tests/Positions/Resizing/TopRightResizerProviderTests.cs @@ -0,0 +1,82 @@ +using Blazor.Diagrams.Core.Controls.Default; +using Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; +using Blazor.Diagrams.Core.Models; +using Blazor.Diagrams.Core.Positions.Resizing; +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Blazor.Diagrams.Core.Tests.Positions.Resizing +{ + public class TopRightResizerProviderTests + { + [Fact] + public void DragResizer_ShouldResizeNode() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new TopRightResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(10, 15, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(15); + node.Size.Width.Should().Be(110); + node.Size.Height.Should().Be(185); + } + + [Fact] + public void DragResizer_SmallerThanMinSize_SetsNodeToMinSize() + { + // setup + var diagram = new TestDiagram(); + diagram.SetContainer(new Rectangle(0, 0, 1000, 400)); + var node = new NodeModel(position: new Point(0, 0)); + node.Size = new Size(100, 200); + var control = new ResizeControl(new TopRightResizerProvider()); + diagram.Controls.AddFor(node).Add(control); + diagram.SelectModel(node, false); + + // before resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(0); + node.Size.Width.Should().Be(100); + node.Size.Height.Should().Be(200); + + // resize + var eventArgs = new PointerEventArgs(0, 0, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + control.OnPointerDown(diagram, node, eventArgs); + eventArgs = new PointerEventArgs(-99, 199, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + eventArgs = new PointerEventArgs(-300, 300, 0, 0, false, false, false, 1, 1, 1, 1, 1, 1, "arrow", true); + diagram.TriggerPointerMove(null, eventArgs); + + // after resize + node.Position.X.Should().Be(0); + node.Position.Y.Should().Be(199); + node.Size.Width.Should().Be(0); + node.Size.Height.Should().Be(0); + } + } +} diff --git a/tests/Blazor.Diagrams.Tests/Components/Controls/ResizeControlWidgetTests.cs b/tests/Blazor.Diagrams.Tests/Components/Controls/ResizeControlWidgetTests.cs new file mode 100644 index 000000000..bf51e541c --- /dev/null +++ b/tests/Blazor.Diagrams.Tests/Components/Controls/ResizeControlWidgetTests.cs @@ -0,0 +1,25 @@ +using Blazor.Diagrams.Components.Controls; +using Blazor.Diagrams.Core.Controls.Default; +using Blazor.Diagrams.Core.Positions.Resizing; +using Bunit; +using Moq; +using Xunit; + +namespace Blazor.Diagrams.Tests.Components.Controls +{ + public class ResizeControlWidgetTests + { + [Fact] + public void ShouldRenderDiv() + { + using var ctx = new TestContext(); + var providerMock = Mock.Of(); + + var cut = ctx.RenderComponent(parameters => + parameters.Add(w => w.Control, new ResizeControl(providerMock)) + ); + + cut.MarkupMatches("
"); + } + } +}