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; /// /// Presents a control within a panel in which to zoom in and out and pan. The must be a , /// and its and must be set. /// [TemplatePart("PART_ZoomContent", typeof(ContentControl))] [TemplatePart("PART_ZoomCanvas", typeof(Canvas))] [TemplatePart("PART_ZoomContentBorder", typeof(Border))] public partial class ZoomPanel : TemplatedControl { public static readonly StyledProperty ContentProperty = AvaloniaProperty.Register(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("PART_ZoomCanvas"); ZoomContent = e.NameScope.Get("PART_ZoomContent"); ZoomContentBorder = e.NameScope.Get("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; } }