diff --git a/FModel/Settings/UserSettings.cs b/FModel/Settings/UserSettings.cs index d1a9f007..6d3d9d42 100644 --- a/FModel/Settings/UserSettings.cs +++ b/FModel/Settings/UserSettings.cs @@ -36,7 +36,8 @@ namespace FModel.Settings private static bool _bSave = true; public static void Save() { - if (!_bSave || Default == null) return; + if (!_bSave || Default == null) + return; Default.PerDirectory[Default.CurrentDir.GameDirectory] = Default.CurrentDir; File.WriteAllText(FilePath, JsonConvert.SerializeObject(Default, Formatting.Indented)); } @@ -147,7 +148,7 @@ namespace FModel.Settings set => SetProperty(ref _isLoggerExpanded, value); } - private GridLength _avalonImageSize = new (200); + private GridLength _avalonImageSize = new(200); public GridLength AvalonImageSize { get => _avalonImageSize; @@ -300,7 +301,7 @@ namespace FModel.Settings set => SetProperty(ref _manualGames, value); } - private AuthResponse _lastAuthResponse = new() {AccessToken = "", ExpiresAt = DateTime.Now}; + private AuthResponse _lastAuthResponse = new() { AccessToken = "", ExpiresAt = DateTime.Now }; public AuthResponse LastAuthResponse { get => _lastAuthResponse; diff --git a/FModel/Views/Resources/Controls/ImagePopout.xaml b/FModel/Views/Resources/Controls/ImagePopout.xaml index 99847efd..f8044756 100644 --- a/FModel/Views/Resources/Controls/ImagePopout.xaml +++ b/FModel/Views/Resources/Controls/ImagePopout.xaml @@ -1,16 +1,14 @@ - - - - - + + - - + + - + diff --git a/FModel/Views/Resources/Controls/ImagePopout.xaml.cs b/FModel/Views/Resources/Controls/ImagePopout.xaml.cs index 32287b88..01682f61 100644 --- a/FModel/Views/Resources/Controls/ImagePopout.xaml.cs +++ b/FModel/Views/Resources/Controls/ImagePopout.xaml.cs @@ -1,9 +1,12 @@ +using Avalonia.Controls; + namespace FModel.Views.Resources.Controls; -public partial class ImagePopout +public partial class ImagePopout : Window { public ImagePopout() { InitializeComponent(); + MagnifierManager.SetMagnifier(RootPanel, new Magnifier { Radius = 150, ZoomFactor = 0.7 }); } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Mgn/Magnifier.cs b/FModel/Views/Resources/Controls/Mgn/Magnifier.cs index b58ce24b..e3fbb830 100644 --- a/FModel/Views/Resources/Controls/Mgn/Magnifier.cs +++ b/FModel/Views/Resources/Controls/Mgn/Magnifier.cs @@ -1,176 +1,230 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; namespace FModel.Views.Resources.Controls; -[TemplatePart(Name = PART_VisualBrush, Type = typeof(VisualBrush))] public class Magnifier : Control { private const double DEFAULT_SIZE = 100d; - private const string PART_VisualBrush = "PART_VisualBrush"; - private VisualBrush _visualBrush = new(); - public static readonly DependencyProperty FrameTypeProperty = - DependencyProperty.Register("FrameType", typeof(EFrameType), typeof(Magnifier), new UIPropertyMetadata(EFrameType.Circle, OnFrameTypeChanged)); + // ----------------------------------------------------------------------- + // Styled properties + // ----------------------------------------------------------------------- + + public static readonly StyledProperty FrameTypeProperty = + AvaloniaProperty.Register(nameof(FrameType), defaultValue: EFrameType.Circle); public EFrameType FrameType { - get => (EFrameType) GetValue(FrameTypeProperty); + get => GetValue(FrameTypeProperty); set => SetValue(FrameTypeProperty, value); } - private static void OnFrameTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var m = (Magnifier) d; - m.OnFrameTypeChanged((EFrameType) e.OldValue, (EFrameType) e.NewValue); - } - - protected virtual void OnFrameTypeChanged(EFrameType oldValue, EFrameType newValue) - { - UpdateSizeFromRadius(); - } - - public static readonly DependencyProperty IsUsingZoomOnMouseWheelProperty = - DependencyProperty.Register("IsUsingZoomOnMouseWheel", typeof(bool), typeof(Magnifier), new UIPropertyMetadata(true)); + public static readonly StyledProperty IsUsingZoomOnMouseWheelProperty = + AvaloniaProperty.Register(nameof(IsUsingZoomOnMouseWheel), defaultValue: true); public bool IsUsingZoomOnMouseWheel { - get => (bool) GetValue(IsUsingZoomOnMouseWheelProperty); + get => GetValue(IsUsingZoomOnMouseWheelProperty); set => SetValue(IsUsingZoomOnMouseWheelProperty, value); } - public bool IsFrozen { get; private set; } - - public static readonly DependencyProperty RadiusProperty = - DependencyProperty.Register("Radius", typeof(double), typeof(Magnifier), new FrameworkPropertyMetadata(DEFAULT_SIZE / 2, OnRadiusPropertyChanged)); + public static readonly StyledProperty RadiusProperty = + AvaloniaProperty.Register(nameof(Radius), defaultValue: DEFAULT_SIZE / 2); public double Radius { - get => (double) GetValue(RadiusProperty); + get => GetValue(RadiusProperty); set => SetValue(RadiusProperty, value); } - private static void OnRadiusPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + public static readonly StyledProperty TargetProperty = + AvaloniaProperty.Register(nameof(Target)); + public Control? Target { - var m = (Magnifier) d; - m.OnRadiusChanged(e); - } - - protected virtual void OnRadiusChanged(DependencyPropertyChangedEventArgs e) - { - UpdateSizeFromRadius(); - } - - public static readonly DependencyProperty TargetProperty = DependencyProperty.Register("Target", typeof(UIElement), typeof(Magnifier)); - public UIElement Target - { - get => (UIElement) GetValue(TargetProperty); + get => GetValue(TargetProperty); set => SetValue(TargetProperty, value); } - public Rect ViewBox - { - get => _visualBrush.Viewbox; - set => _visualBrush.Viewbox = value; - } - - public static readonly DependencyProperty ZoomFactorProperty = - DependencyProperty.Register("ZoomFactor", typeof(double), typeof(Magnifier), new FrameworkPropertyMetadata(0.5, OnZoomFactorPropertyChanged), OnValidationCallback); + public static readonly StyledProperty ZoomFactorProperty = + AvaloniaProperty.Register(nameof(ZoomFactor), defaultValue: 0.5, + validate: v => v >= 0); public double ZoomFactor { - get => (double) GetValue(ZoomFactorProperty); + get => GetValue(ZoomFactorProperty); set => SetValue(ZoomFactorProperty, value); } - private static bool OnValidationCallback(object baseValue) - { - return (double) baseValue >= 0; - } - - private static void OnZoomFactorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var m = (Magnifier) d; - m.OnZoomFactorChanged(e); - } - - protected virtual void OnZoomFactorChanged(DependencyPropertyChangedEventArgs e) - { - UpdateViewBox(); - } - - public static readonly DependencyProperty ZoomFactorOnMouseWheelProperty = - DependencyProperty.Register("ZoomFactorOnMouseWheel", typeof(double), typeof(Magnifier), new FrameworkPropertyMetadata(0.1d, OnZoomFactorOnMouseWheelPropertyChanged), OnZoomFactorOnMouseWheelValidationCallback); + public static readonly StyledProperty ZoomFactorOnMouseWheelProperty = + AvaloniaProperty.Register(nameof(ZoomFactorOnMouseWheel), defaultValue: 0.1d, + validate: v => v >= 0); public double ZoomFactorOnMouseWheel { - get => (double) GetValue(ZoomFactorOnMouseWheelProperty); + get => GetValue(ZoomFactorOnMouseWheelProperty); set => SetValue(ZoomFactorOnMouseWheelProperty, value); } - private static bool OnZoomFactorOnMouseWheelValidationCallback(object baseValue) + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background)); + public IBrush? Background { - return (double) baseValue >= 0; + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); } - private static void OnZoomFactorOnMouseWheelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + public static readonly StyledProperty BorderBrushProperty = + AvaloniaProperty.Register(nameof(BorderBrush)); + public IBrush? BorderBrush { - var m = (Magnifier) d; - m.OnZoomFactorOnMouseWheelChanged(e); + get => GetValue(BorderBrushProperty); + set => SetValue(BorderBrushProperty, value); } - protected virtual void OnZoomFactorOnMouseWheelChanged(DependencyPropertyChangedEventArgs e) + public static readonly StyledProperty BorderThicknessProperty = + AvaloniaProperty.Register(nameof(BorderThickness)); + public Thickness BorderThickness { + get => GetValue(BorderThicknessProperty); + set => SetValue(BorderThicknessProperty, value); } + // ViewBox is not a styled property — it is driven programmatically by the adorner. + public Rect ViewBox { get; set; } + + public bool IsFrozen { get; private set; } + + // VisualBrush paints the magnified region; rebuilt whenever Target changes. + private VisualBrush? _visualBrush; + + // ----------------------------------------------------------------------- + // Static ctor — wire property-changed callbacks + // ----------------------------------------------------------------------- static Magnifier() { - DefaultStyleKeyProperty.OverrideMetadata(typeof(Magnifier), new FrameworkPropertyMetadata(typeof(Magnifier))); - HeightProperty.OverrideMetadata(typeof(Magnifier), new FrameworkPropertyMetadata(DEFAULT_SIZE)); - WidthProperty.OverrideMetadata(typeof(Magnifier), new FrameworkPropertyMetadata(DEFAULT_SIZE)); - } + FrameTypeProperty.Changed.AddClassHandler((m, _) => m.OnFrameTypeChanged()); + RadiusProperty.Changed.AddClassHandler((m, _) => m.OnRadiusChanged()); + ZoomFactorProperty.Changed.AddClassHandler((m, _) => m.UpdateViewBox()); + // Rebuild the VisualBrush whenever the Target control changes. + TargetProperty.Changed.AddClassHandler((m, _) => m.RebuildBrush()); - public Magnifier() - { - SizeChanged += OnSizeChangedEvent; - } + // Default Width / Height + WidthProperty.OverrideDefaultValue(DEFAULT_SIZE); + HeightProperty.OverrideDefaultValue(DEFAULT_SIZE); - private void OnSizeChangedEvent(object sender, SizeChangedEventArgs e) - { - UpdateViewBox(); - } - - private void UpdateSizeFromRadius() - { - if (FrameType != EFrameType.Circle) return; - - var newSize = Radius * 2; - if (!Helper.AreVirtuallyEqual(Width, newSize)) + // Class-level handler — must live in static ctor, not the instance ctor, to avoid + // registering N handlers for N instances (AddClassHandler is class-wide). + BoundsProperty.Changed.AddClassHandler((m, e) => { - Width = newSize; + if (e.OldValue is Rect oldBounds && e.NewValue is Rect newBounds && oldBounds.Size == newBounds.Size) + return; + m.UpdateViewBox(); // UpdateViewBox already calls InvalidateVisual(). + }); + } + + public Magnifier() { } + + // ----------------------------------------------------------------------- + // Overrides + // ----------------------------------------------------------------------- + public override void Render(DrawingContext context) + { + if (_visualBrush == null || Bounds.Width <= 0 || Bounds.Height <= 0) + return; + + var strokeThickness = (BorderThickness.Left + BorderThickness.Top + + BorderThickness.Right + BorderThickness.Bottom) / 4; + var pen = BorderBrush != null ? new Pen(BorderBrush, strokeThickness) : null; + + if (FrameType == EFrameType.Circle) + { + var center = new Point(Bounds.Width / 2, Bounds.Height / 2); + var rx = Bounds.Width / 2; + var ry = Bounds.Height / 2; + if (Background != null) + context.DrawEllipse(Background, null, center, rx, ry); + context.DrawEllipse(_visualBrush, pen, center, rx, ry); } - - if (!Helper.AreVirtuallyEqual(Height, newSize)) + else { - Height = newSize; + var rect = new Rect(0, 0, Bounds.Width, Bounds.Height); + if (Background != null) + context.DrawRectangle(Background, null, rect); + context.DrawRectangle(_visualBrush, pen, rect); } } - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - var newBrush = GetTemplateChild(PART_VisualBrush) as VisualBrush ?? new VisualBrush(); - newBrush.Viewbox = _visualBrush.Viewbox; - _visualBrush = newBrush; - } - + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- public void Freeze(bool freeze) { IsFrozen = freeze; } - private void UpdateViewBox() + private void OnFrameTypeChanged() { - if (!IsInitialized) + UpdateSizeFromRadius(); + InvalidateVisual(); + } + + private void OnRadiusChanged() + { + UpdateSizeFromRadius(); + } + + private void UpdateSizeFromRadius() + { + if (FrameType != EFrameType.Circle) return; - ViewBox = new Rect(ViewBox.Location, new Size(ActualWidth * ZoomFactor, ActualHeight * ZoomFactor)); + var newSize = Radius * 2; + if (!Helper.AreVirtuallyEqual(Width, newSize)) + Width = newSize; + if (!Helper.AreVirtuallyEqual(Height, newSize)) + Height = newSize; } -} \ No newline at end of file + + internal void UpdateViewBox() + { + // Prefer actual layout size; fall back to the Width/Height styled properties + // (default 100) so that ViewBox.Size is non-zero even before the first layout + // pass. This ensures CalculateViewBoxLocation() can center the viewport under + // the cursor on the very first PointerPressed before Bounds are measured. + var w = Bounds.Width > 0 ? Bounds.Width : Width; + var h = Bounds.Height > 0 ? Bounds.Height : Height; + if (w <= 0 || h <= 0) + return; + + ViewBox = new Rect(ViewBox.X, ViewBox.Y, w * ZoomFactor, h * ZoomFactor); + UpdateBrushViewBox(); + InvalidateVisual(); + } + + private void RebuildBrush() + { + if (Target == null) + { + _visualBrush = null; + InvalidateVisual(); + return; + } + + _visualBrush = new VisualBrush + { + SourceControl = Target, + Stretch = Stretch.Fill, + TileMode = TileMode.None, + AlignmentX = AlignmentX.Left, + AlignmentY = AlignmentY.Top, + DestinationRect = new RelativeRect(0, 0, 1, 1, RelativeUnit.Relative), + }; + UpdateBrushViewBox(); + InvalidateVisual(); + } + + private void UpdateBrushViewBox() + { + if (_visualBrush == null) + return; + // SourceRect maps which region of Target is painted — the magnified "window". + _visualBrush.SourceRect = new RelativeRect(ViewBox, RelativeUnit.Absolute); + } +} diff --git a/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs b/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs index 04d0bd68..f4790429 100644 --- a/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs +++ b/FModel/Views/Resources/Controls/Mgn/MagnifierAdorner.cs @@ -1,85 +1,121 @@ -using System.Windows; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; namespace FModel.Views.Resources.Controls; -public class MagnifierAdorner : Adorner +/// +/// Avalonia replacement for WPF MagnifierAdorner. +/// Placed in the AdornerLayer as a Canvas that positions the at the cursor. +/// +public class MagnifierAdorner : Canvas { - private Magnifier _magnifier; - private Point _currentMousePosition; + private readonly Control _adornedElement; + private readonly Magnifier _magnifier; + private Point _currentPointerPosition; + private Point _currentElementPosition; private double _currentZoomFactor; - public MagnifierAdorner(UIElement element, Magnifier magnifier) : base(element) + public MagnifierAdorner(Control adornedElement, Magnifier magnifier) { + _adornedElement = adornedElement; _magnifier = magnifier; - _currentZoomFactor = _magnifier.ZoomFactor; + _currentZoomFactor = magnifier.ZoomFactor; + // The canvas must be transparent to hit-testing so pointer events reach the adorned control. + IsHitTestVisible = false; + // Start hidden so the first ShowAdorner() call triggers IsVisible false→true, + // which fires OnPropertyChanged and subscribes PointerMoved correctly. + IsVisible = false; + + Children.Add(_magnifier); UpdateViewBox(); - AddVisualChild(_magnifier); - Loaded += (_, _) => InputManager.Current.PostProcessInput += OnProcessInput; - Unloaded += (_, _) => InputManager.Current.PostProcessInput -= OnProcessInput; + // Subscribe to pointer events on the adorned element. + // PointerPressed fires first (manager handles ShowAdorner before this handler runs, + // so the adorner is already in the visual tree when we call e.GetPosition(this)). + // PointerMoved is subscribed/unsubscribed dynamically via OnPropertyChanged(IsVisible). + _adornedElement.PointerPressed += OnAdornedElementPointerPressed; } - private void OnProcessInput(object sender, ProcessInputEventArgs e) + public void Detach() { - var pt = Mouse.GetPosition(this); - if (_currentMousePosition == pt && _magnifier.ZoomFactor == _currentZoomFactor) + _adornedElement.PointerPressed -= OnAdornedElementPointerPressed; + _adornedElement.PointerMoved -= OnAdornedElementPointerMoved; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == IsVisibleProperty) + { + if (change.GetNewValue()) + _adornedElement.PointerMoved += OnAdornedElementPointerMoved; + else + _adornedElement.PointerMoved -= OnAdornedElementPointerMoved; + } + } + + private void OnAdornedElementPointerPressed(object? sender, PointerPressedEventArgs e) + => HandlePointerEvent(e); + + private void OnAdornedElementPointerMoved(object? sender, PointerEventArgs e) + => HandlePointerEvent(e); + + private void HandlePointerEvent(PointerEventArgs e) + { + // Adorner-canvas-relative position (used for magnifier placement on the canvas). + var pt = e.GetPosition(this); + + if (_currentPointerPosition == pt && _magnifier.ZoomFactor == _currentZoomFactor) return; if (_magnifier.IsFrozen) return; - _currentMousePosition = pt; + _currentPointerPosition = pt; + // Element-relative position (used for viewbox origin calculation — avoids PointToScreen round-trip). + _currentElementPosition = e.GetPosition(_adornedElement); _currentZoomFactor = _magnifier.ZoomFactor; UpdateViewBox(); - InvalidateArrange(); + PositionMagnifier(); } public void UpdateViewBox() { - var viewBoxLocation = CalculateViewBoxLocation(); - _magnifier.ViewBox = new Rect(viewBoxLocation, _magnifier.ViewBox.Size); + var location = CalculateViewBoxLocation(); + _magnifier.ViewBox = new Rect(location, _magnifier.ViewBox.Size); + _magnifier.UpdateViewBox(); } private Point CalculateViewBoxLocation() { - double offsetX, offsetY; - var adorner = Mouse.GetPosition(this); - var element = Mouse.GetPosition(AdornedElement); + // offsetX/offsetY = coordinate delta between adorner-canvas space and adorned-element space. + // Both positions come from the same PointerEventArgs so they share the same root transform, + // making this DPI-safe without any PointToScreen / PointToClient round-trip. + var offsetX = _currentElementPosition.X - _currentPointerPosition.X; + var offsetY = _currentElementPosition.Y - _currentPointerPosition.Y; - offsetX = element.X - adorner.X; - offsetY = element.Y - adorner.Y; + // Account for the target control's offset within its parent coordinate space. + Point parentOffset = default; + if (_magnifier.Target != null) + { + var offsetVec = _magnifier.Target.TranslatePoint(default, _adornedElement); + if (offsetVec.HasValue) + parentOffset = offsetVec.Value; + } - var parentOffsetVector = VisualTreeHelper.GetOffset(_magnifier.Target); - var parentOffset = new Point(parentOffsetVector.X, parentOffsetVector.Y); - - var left = _currentMousePosition.X - (_magnifier.ViewBox.Width / 2 + offsetX) + parentOffset.X; - var top = _currentMousePosition.Y - (_magnifier.ViewBox.Height / 2 + offsetY) + parentOffset.Y; + var left = _currentPointerPosition.X - (_magnifier.ViewBox.Width / 2 + offsetX) + parentOffset.X; + var top = _currentPointerPosition.Y - (_magnifier.ViewBox.Height / 2 + offsetY) + parentOffset.Y; return new Point(left, top); } - protected override Visual GetVisualChild(int index) + private void PositionMagnifier() { - return _magnifier; + var x = _currentPointerPosition.X - _magnifier.Width / 2; + var y = _currentPointerPosition.Y - _magnifier.Height / 2; + SetLeft(_magnifier, x); + SetTop(_magnifier, y); } - - protected override int VisualChildrenCount => 1; - - protected override Size MeasureOverride(Size constraint) - { - _magnifier.Measure(constraint); - return base.MeasureOverride(constraint); - } - - protected override Size ArrangeOverride(Size finalSize) - { - var x = _currentMousePosition.X - _magnifier.Width / 2; - var y = _currentMousePosition.Y - _magnifier.Height / 2; - _magnifier.Arrange(new Rect(x, y, _magnifier.Width, _magnifier.Height)); - return base.ArrangeOverride(finalSize); - } -} \ No newline at end of file +} diff --git a/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs b/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs index ec0c9b14..eec7d872 100644 --- a/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs +++ b/FModel/Views/Resources/Controls/Mgn/MagnifierManager.cs @@ -1,102 +1,167 @@ using System; -using System.Windows; -using System.Windows.Documents; -using System.Windows.Input; +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; namespace FModel.Views.Resources.Controls; -public class MagnifierManager : DependencyObject +public class MagnifierManager { - private MagnifierAdorner _adorner; - private UIElement _element; + private MagnifierAdorner? _adorner; + private Control? _element; - public static readonly DependencyProperty CurrentProperty = - DependencyProperty.RegisterAttached("Magnifier", typeof(Magnifier), typeof(UIElement), new FrameworkPropertyMetadata(null, OnMagnifierChanged)); + // Stores active managers so OnMagnifierChanged can detach the old one when the property changes. + private static readonly ConditionalWeakTable _managers = new(); - public static void SetMagnifier(UIElement element, Magnifier value) + // ----------------------------------------------------------------------- + // Attached property — usage: + // ----------------------------------------------------------------------- + public static readonly AttachedProperty MagnifierProperty = + AvaloniaProperty.RegisterAttached("Magnifier"); + + public static void SetMagnifier(Control element, Magnifier? value) + => element.SetValue(MagnifierProperty, value); + + public static Magnifier? GetMagnifier(Control element) + => element.GetValue(MagnifierProperty); + + static MagnifierManager() { - element.SetValue(CurrentProperty, value); + MagnifierProperty.Changed.Subscribe(OnMagnifierChanged); } - public static Magnifier GetMagnifier(UIElement element) + private static void OnMagnifierChanged(AvaloniaPropertyChangedEventArgs e) { - return (Magnifier) element.GetValue(CurrentProperty); - } + if (e.Sender is not Control target) + throw new ArgumentException("Magnifier can only be attached to a Control."); - private static void OnMagnifierChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not UIElement target) - throw new ArgumentException("Magnifier can only be attached to a UIElement."); - - new MagnifierManager().AttachToMagnifier(target, e.NewValue as Magnifier); - } - - private void ElementOnMouseLeftButtonUp(object sender, MouseEventArgs e) - { - if (GetMagnifier(_element) is { IsFrozen: true }) - return; - - HideAdorner(); - } - - private void ElementOnMouseLeftButtonDown(object sender, MouseEventArgs e) - { - ShowAdorner(); - } - - private void ElementOnMouseWheel(object sender, MouseWheelEventArgs e) - { - if (GetMagnifier(_element) is not { IsUsingZoomOnMouseWheel: true } magnifier) return; - - switch (e.Delta) + // Detach any previously registered manager on this element before attaching a new one. + if (_managers.TryGetValue(target, out var old)) { - case < 0: - { - var newValue = magnifier.ZoomFactor + magnifier.ZoomFactorOnMouseWheel; - magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue); - break; - } - case > 0: - { - var newValue = magnifier.ZoomFactor >= magnifier.ZoomFactorOnMouseWheel ? magnifier.ZoomFactor - magnifier.ZoomFactorOnMouseWheel : 0d; - magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue); - break; - } + old.Detach(); + _managers.Remove(target); } - _adorner.UpdateViewBox(); + var magnifier = e.NewValue.GetValueOrDefault(); + if (magnifier != null) + { + var manager = new MagnifierManager(); + manager.AttachToMagnifier(target, magnifier); + _managers.Add(target, manager); + } } - private void AttachToMagnifier(UIElement element, Magnifier magnifier) + // ----------------------------------------------------------------------- + // Instance — manages one control/magnifier pair + // ----------------------------------------------------------------------- + + private void AttachToMagnifier(Control element, Magnifier magnifier) { _element = element; - _element.MouseLeftButtonDown += ElementOnMouseLeftButtonDown; - _element.MouseLeftButtonUp += ElementOnMouseLeftButtonUp; - _element.MouseWheel += ElementOnMouseWheel; + _element.PointerPressed += ElementOnPointerPressed; + _element.PointerReleased += ElementOnPointerReleased; + _element.PointerWheelChanged += ElementOnPointerWheelChanged; + _element.DetachedFromVisualTree += OnElementDetached; magnifier.Target = _element; _adorner = new MagnifierAdorner(_element, magnifier); } + private void Detach() + { + if (_element != null) + { + _element.PointerPressed -= ElementOnPointerPressed; + _element.PointerReleased -= ElementOnPointerReleased; + _element.PointerWheelChanged -= ElementOnPointerWheelChanged; + _element.DetachedFromVisualTree -= OnElementDetached; + + // Remove the adorner control from the AdornerLayer so it is not left + // as an invisible orphan child for the lifetime of the host window. + // Use the static API to match the add path in VerifyAdornerLayer. + AdornerLayer.SetAdornment(_element, null); + } + _adorner?.Detach(); + _adorner = null; + _element = null; + } + + private void OnElementDetached(object? sender, VisualTreeAttachmentEventArgs e) + { + // Full teardown so that if the element is ever re-attached and SetMagnifier is + // called again, a fresh manager/adorner pair is created from scratch. + var element = _element; + Detach(); + if (element != null) + _managers.Remove(element); + } + + private void ElementOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.GetCurrentPoint(null).Properties.PointerUpdateKind != PointerUpdateKind.LeftButtonReleased) + return; + if (_element != null && GetMagnifier(_element) is { IsFrozen: true }) + return; + + HideAdorner(); + } + + private void ElementOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(null).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonPressed) + ShowAdorner(); + } + + private void ElementOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) + { + if (_element == null) + return; + if (GetMagnifier(_element) is not { IsUsingZoomOnMouseWheel: true } magnifier) + return; + + // WPF Delta < 0 = scroll down = zoom out (larger viewbox); Delta > 0 = scroll up = zoom in. + // Avalonia Delta.Y > 0 = scroll up. + if (e.Delta.Y < 0) + { + var newValue = magnifier.ZoomFactor + magnifier.ZoomFactorOnMouseWheel; + magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue); + } + else if (e.Delta.Y > 0) + { + var newValue = magnifier.ZoomFactor >= magnifier.ZoomFactorOnMouseWheel + ? magnifier.ZoomFactor - magnifier.ZoomFactorOnMouseWheel + : 0d; + magnifier.SetCurrentValue(Magnifier.ZoomFactorProperty, newValue); + } + + _adorner?.UpdateViewBox(); + } + private void ShowAdorner() { + if (_adorner == null || _element == null) + return; VerifyAdornerLayer(); - _adorner.Visibility = Visibility.Visible; + _adorner.IsVisible = true; } private void VerifyAdornerLayer() { - if (_adorner.Parent != null) return; + if (_adorner == null || _element == null) + return; + if (_adorner.Parent != null) + return; + var layer = AdornerLayer.GetAdornerLayer(_element); - layer?.Add(_adorner); + if (layer != null) + AdornerLayer.SetAdornment(_element, _adorner); } private void HideAdorner() { - if (_adorner.Visibility == Visibility.Visible) - { - _adorner.Visibility = Visibility.Collapsed; - } + if (_adorner is { IsVisible: true }) + _adorner.IsVisible = false; } -} \ No newline at end of file +}