| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 | 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;using System;namespace InABox.Avalonia.Components;/// <summary>/// Presents a control within a panel in which to zoom in and out and pan. The <see cref="Content"/> must be a <see cref="Layoutable"/>,/// and its <see cref="Layoutable.Width"/> and <see cref="Layoutable.Height"/> <b>must</b> be set./// </summary>[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();    public ZoomPanel()    {        this.GetPropertyChangedObservable(ContentProperty).Subscribe(ContentChanged);    }    private void ContentChanged(AvaloniaPropertyChangedEventArgs args)    {        if(Content is null) return;        void Update(AvaloniaPropertyChangedEventArgs? args = null)        {            if(OuterCanvas is not null)            {                PositionContent();            }        }        Update();        Content.GetPropertyChangedObservable(Layoutable.WidthProperty).Subscribe(Update);        Content.GetPropertyChangedObservable(Layoutable.HeightProperty).Subscribe(Update);    }    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;    }}
 |