using Avalonia; using Avalonia.Input; using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; namespace InABox.Avalonia.Components.ImageEditing; public class PanAndZoomEventArgs : RoutedEventArgs { public PanAndZoomEventArgs(double scale, Point scaleOrigin, Vector pan) : base(PanAndZoomGestureRecognizer.PanAndZoomEvent) { Scale = scale; ScaleOrigin = scaleOrigin; Pan = pan; } public double Scale { get; } = 1; public Point ScaleOrigin { get; } public Vector Pan { get; set; } } public class PanAndZoomEndedEventArgs : RoutedEventArgs { public PanAndZoomEndedEventArgs() : base(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent) { } } // Adapted from the PinchGestureRecognizer built in to Avalonia, but with panning added. public class PanAndZoomGestureRecognizer : GestureRecognizer { public static readonly RoutedEvent PanAndZoomEvent = RoutedEvent.Register( "PanAndZoom", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer)); public static readonly RoutedEvent PanAndZoomEndedEvent = RoutedEvent.Register( "PanAndZoomEnded", RoutingStrategies.Bubble, typeof(PanAndZoomGestureRecognizer)); private float _initialDistance; private IPointer? _firstContact; private Point _firstPoint; private IPointer? _secondContact; private Point _secondPoint; private Point _previousOrigin; protected override void PointerCaptureLost(IPointer pointer) { RemoveContact(pointer); } protected override void PointerMoved(PointerEventArgs e) { if (Target is Visual visual) { if (_firstContact == e.Pointer) { _firstPoint = e.GetPosition(visual); } else if (_secondContact == e.Pointer) { _secondPoint = e.GetPosition(visual); } else { return; } if (_firstContact != null && _secondContact != null) { var distance = GetDistance(_firstPoint, _secondPoint); var scale = distance / _initialDistance; var origin = (_firstPoint + _secondPoint) / 2; var pinchEventArgs = new PanAndZoomEventArgs(scale, origin, origin - _previousOrigin); _previousOrigin = origin; Target?.RaiseEvent(pinchEventArgs); e.Handled = pinchEventArgs.Handled; e.PreventGestureRecognition(); } } } protected override void PointerPressed(PointerPressedEventArgs e) { if (Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) { if (_firstContact == null) { _firstContact = e.Pointer; _firstPoint = e.GetPosition(visual); return; } else if (_secondContact == null && _firstContact != e.Pointer) { _secondContact = e.Pointer; _secondPoint = e.GetPosition(visual); } else { return; } if (_firstContact != null && _secondContact != null) { _initialDistance = GetDistance(_firstPoint, _secondPoint); _previousOrigin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f); Capture(_firstContact); Capture(_secondContact); e.PreventGestureRecognition(); } } } protected override void PointerReleased(PointerReleasedEventArgs e) { if(RemoveContact(e.Pointer)) { e.PreventGestureRecognition(); } } private bool RemoveContact(IPointer pointer) { if (_firstContact == pointer || _secondContact == pointer) { if (_secondContact == pointer) { _secondContact = null; } if (_firstContact == pointer) { _firstContact = _secondContact; _secondContact = null; } Target?.RaiseEvent(new PanAndZoomEndedEventArgs()); return true; } return false; } private static float GetDistance(Point a, Point b) { var length = b - a; return (float)new Vector(length.X, length.Y).Length; } }