|
@@ -0,0 +1,155 @@
|
|
|
+using Avalonia;
|
|
|
+using Avalonia.Controls;
|
|
|
+using Avalonia.Controls.Metadata;
|
|
|
+using Avalonia.Controls.Primitives;
|
|
|
+using Avalonia.Input;
|
|
|
+using Avalonia.Interactivity;
|
|
|
+using Avalonia.Layout;
|
|
|
+using Avalonia.Markup.Xaml;
|
|
|
+using Avalonia.Media;
|
|
|
+using Avalonia.Metadata;
|
|
|
+
|
|
|
+namespace InABox.Avalonia.Components;
|
|
|
+
|
|
|
+[TemplatePart("PART_ZoomContent", typeof(ContentControl))]
|
|
|
+[TemplatePart("PART_ZoomCanvas", typeof(Canvas))]
|
|
|
+[TemplatePart("PART_ZoomContentBorder", typeof(Border))]
|
|
|
+public partial class ZoomPanel : TemplatedControl
|
|
|
+{
|
|
|
+ public static readonly StyledProperty<Layoutable?> ContentProperty =
|
|
|
+ AvaloniaProperty.Register<ZoomPanel, Layoutable?>(nameof(Content));
|
|
|
+
|
|
|
+ [Content]
|
|
|
+ public Layoutable? Content
|
|
|
+ {
|
|
|
+ get => GetValue(ContentProperty);
|
|
|
+ set => SetValue(ContentProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private double ContentWidth => Content?.Width ?? 1;
|
|
|
+
|
|
|
+ private double ContentHeight => Content?.Height ?? 1;
|
|
|
+
|
|
|
+ private Canvas OuterCanvas = null!;
|
|
|
+ private ContentControl ZoomContent = null!;
|
|
|
+ private Border ZoomContentBorder = null!;
|
|
|
+
|
|
|
+ private double ScaleFactor = 1.0;
|
|
|
+ private double _originalScaleFactor = 1.0;
|
|
|
+
|
|
|
+ private const double _wheelSpeed = 0.1;
|
|
|
+ private const double _panSpeed = 30;
|
|
|
+
|
|
|
+ // Center of the image.
|
|
|
+ private Point ContentCentre = new();
|
|
|
+
|
|
|
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
|
|
+ {
|
|
|
+ base.OnApplyTemplate(e);
|
|
|
+
|
|
|
+ OuterCanvas = e.NameScope.Get<Canvas>("PART_ZoomCanvas");
|
|
|
+ ZoomContent = e.NameScope.Get<ContentControl>("PART_ZoomContent");
|
|
|
+ ZoomContentBorder = e.NameScope.Get<Border>("PART_ZoomContentBorder");
|
|
|
+
|
|
|
+ OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
|
|
|
+ OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
|
|
|
+ OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
|
|
|
+
|
|
|
+ OuterCanvas.PointerWheelChanged += OuterCanvas_PointerWheelChanged;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
|
|
|
+ {
|
|
|
+ _originalScaleFactor = ScaleFactor;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
|
|
|
+ {
|
|
|
+ Zoom(e.ScaleOrigin - e.Pan, e.ScaleOrigin, _originalScaleFactor * e.Scale);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Zoom(Point originalOrigin, Point newOrigin, double newScaleFactor)
|
|
|
+ {
|
|
|
+ // Convert Scale Origin to image coordinates (relative to center).
|
|
|
+ // Work out where this position will move to under the new scaling.
|
|
|
+ // Adjust so that these are the same.
|
|
|
+
|
|
|
+ var pos = originalOrigin - ContentCentre;
|
|
|
+ var contentMPos = pos / ScaleFactor;
|
|
|
+
|
|
|
+ ScaleFactor = newScaleFactor;
|
|
|
+
|
|
|
+ var scaledPos = ContentCentre + contentMPos * ScaleFactor;
|
|
|
+ var offset = scaledPos - newOrigin;
|
|
|
+
|
|
|
+ ContentCentre -= offset;
|
|
|
+ UpdateCanvasPosition();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Pan(double x, double y)
|
|
|
+ {
|
|
|
+ ContentCentre += new Vector(x, y);
|
|
|
+ UpdateCanvasPosition();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OuterCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
|
|
|
+ {
|
|
|
+ if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
|
|
+ {
|
|
|
+ var pos = e.GetPosition(OuterCanvas);
|
|
|
+ var wheelSpeed = _wheelSpeed;
|
|
|
+ Zoom(pos, pos, e.Delta.Y > 0 ? ScaleFactor * (1 + e.Delta.Y * wheelSpeed) : ScaleFactor / (1 + (-e.Delta.Y) * wheelSpeed));
|
|
|
+ }
|
|
|
+ else if(e.KeyModifiers.HasFlag(KeyModifiers.Shift))
|
|
|
+ {
|
|
|
+ Pan(e.Delta.Y * _panSpeed, e.Delta.X * _panSpeed);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Pan(e.Delta.X * _panSpeed, e.Delta.Y * _panSpeed);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
|
|
|
+ {
|
|
|
+ // PositionImage();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override void OnLoaded(RoutedEventArgs e)
|
|
|
+ {
|
|
|
+ base.OnLoaded(e);
|
|
|
+ PositionContent();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void PositionContent()
|
|
|
+ {
|
|
|
+ var canvasWidth = OuterCanvas.Bounds.Width;
|
|
|
+ var canvasHeight = OuterCanvas.Bounds.Height;
|
|
|
+
|
|
|
+ var scaleFactor = Math.Min(canvasWidth / ContentWidth, canvasHeight / ContentHeight);
|
|
|
+ ScaleFactor = scaleFactor;
|
|
|
+ _originalScaleFactor = ScaleFactor;
|
|
|
+
|
|
|
+ ContentCentre = new Point(
|
|
|
+ OuterCanvas.Bounds.Width / 2,
|
|
|
+ OuterCanvas.Bounds.Height / 2);
|
|
|
+
|
|
|
+ UpdateCanvasPosition();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateCanvasPosition()
|
|
|
+ {
|
|
|
+ var imageWidth = ContentWidth * ScaleFactor;
|
|
|
+ var imageHeight = ContentHeight * ScaleFactor;
|
|
|
+
|
|
|
+ ZoomContentBorder.Width = imageWidth;
|
|
|
+ ZoomContentBorder.Height = imageHeight;
|
|
|
+
|
|
|
+ Canvas.SetLeft(ZoomContentBorder, ContentCentre.X - imageWidth / 2);
|
|
|
+ Canvas.SetTop(ZoomContentBorder, ContentCentre.Y - imageHeight / 2);
|
|
|
+
|
|
|
+ ZoomContent.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
|
|
|
+ ZoomContent.Width = ContentWidth;
|
|
|
+ ZoomContent.Height = ContentHeight;
|
|
|
+ }
|
|
|
+}
|