From dba0388af7f24d63133d07c3fd6466ec5e6edfb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= <10546952+miloush@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:33:31 +0100 Subject: [PATCH] Internal PointerCaptureChanging event (#19833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * PointerCaptureChanging event * PR feedback * Announce implicit capture to platform --------- Co-authored-by: Jan Kučera Co-authored-by: Max Katz --- src/Avalonia.Base/Input/InputElement.cs | 32 ++++++++++++- src/Avalonia.Base/Input/MouseDevice.cs | 4 +- src/Avalonia.Base/Input/Pointer.cs | 53 ++++++++++++++------- src/Avalonia.Base/Input/PointerEventArgs.cs | 17 ++++++- 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 8ce8e24c19..e20a6cb1af 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -166,6 +166,14 @@ namespace Avalonia.Input nameof(PointerReleased), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + /// + /// Defines the routed event. + /// + internal static readonly RoutedEvent PointerCaptureChangingEvent = + RoutedEvent.Register( + nameof(PointerCaptureChanging), + RoutingStrategies.Direct); + /// /// Defines the routed event. /// @@ -221,6 +229,7 @@ namespace Avalonia.Input PointerMovedEvent.AddClassHandler((x, e) => x.OnPointerMoved(e)); PointerPressedEvent.AddClassHandler((x, e) => x.OnPointerPressed(e)); PointerReleasedEvent.AddClassHandler((x, e) => x.OnPointerReleased(e)); + PointerCaptureChangingEvent.AddClassHandler((x, e) => x.OnPointerCaptureChanging(e)); PointerCaptureLostEvent.AddClassHandler((x, e) => x.OnPointerCaptureLost(e)); PointerWheelChangedEvent.AddClassHandler((x, e) => x.OnPointerWheelChanged(e)); @@ -339,9 +348,19 @@ namespace Avalonia.Input remove { RemoveHandler(PointerReleasedEvent, value); } } + /// + /// Occurs when the control or its child control is about to lose capture, + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control. + /// + internal event EventHandler? PointerCaptureChanging + { + add => AddHandler(PointerCaptureChangingEvent, value); + remove => RemoveHandler(PointerCaptureChangingEvent, value); + } + /// /// Occurs when the control or its child control loses the pointer capture for any reason, - /// event will not be triggered for a parent control if capture was transferred to another child of that parent control + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control. /// public event EventHandler? PointerCaptureLost { @@ -671,6 +690,17 @@ namespace Avalonia.Input } } + /// + /// Invoked when an unhandled reaches an element in its + /// route that is derived from this class. Implement this method to add class handling + /// for this event. + /// + /// Data about the event. + internal virtual void OnPointerCaptureChanging(PointerCaptureChangingEventArgs e) + { + + } + /// /// Invoked when an unhandled reaches an element in its /// route that is derived from this class. Implement this method to add class handling diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index b09f195656..49945f1e8a 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -131,7 +131,7 @@ namespace Avalonia.Input if (source != null) { - _pointer.Capture(source); + _pointer.Capture(source, CaptureSource.Implicit); var settings = ((IInputRoot?)(source as Interactive)?.GetVisualRoot())?.PlatformSettings; if (settings is not null) @@ -206,7 +206,7 @@ namespace Avalonia.Input } finally { - _pointer.Capture(null); + _pointer.Capture(null, CaptureSource.Implicit); _pointer.CaptureGestureRecognizer(null); _pointer.IsGestureRecognitionSkipped = false; _lastMouseDownButton = default; diff --git a/src/Avalonia.Base/Input/Pointer.cs b/src/Avalonia.Base/Input/Pointer.cs index 1f6741a09e..f243a2e382 100644 --- a/src/Avalonia.Base/Input/Pointer.cs +++ b/src/Avalonia.Base/Input/Pointer.cs @@ -6,6 +6,13 @@ using Avalonia.VisualTree; namespace Avalonia.Input { + internal enum CaptureSource + { + Explicit, + Implicit, + Platform + } + public class Pointer : IPointer, IDisposable { private static int s_NextFreePointerId = 1000; @@ -30,46 +37,60 @@ namespace Avalonia.Input protected virtual void PlatformCapture(IInputElement? element) { - + } internal void PlatformCaptureLost() { if (Captured != null) - Capture(null, platformInitiated: true); + Capture(null, CaptureSource.Platform); } public void Capture(IInputElement? control) { - Capture(control, platformInitiated: false); + Capture(control, CaptureSource.Explicit); } - private void Capture(IInputElement? control, bool platformInitiated) + internal void Capture(IInputElement? control, CaptureSource source) { var oldCapture = Captured; if (oldCapture == control) return; - if (oldCapture is Visual v1) - v1.DetachedFromVisualTree -= OnCaptureDetached; + var oldVisual = oldCapture as Visual; + + IInputElement? commonParent = null; + if (oldVisual != null) + { + commonParent = FindCommonParent(control, oldCapture); + foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType()) + { + if (notifyTarget == commonParent) + break; + var args = new PointerCaptureChangingEventArgs(notifyTarget, this, control, source); + notifyTarget.RaiseEvent(args); + if (args.Handled) + return; + } + } + + if (oldVisual != null) + oldVisual.DetachedFromVisualTree -= OnCaptureDetached; Captured = control; - - if (!platformInitiated) + + if (source != CaptureSource.Platform) PlatformCapture(control); - if (oldCapture is Visual v2) - { - var commonParent = FindCommonParent(control, oldCapture); - foreach (var notifyTarget in v2.GetSelfAndVisualAncestors().OfType()) + if (oldVisual != null) + foreach (var notifyTarget in oldVisual.GetSelfAndVisualAncestors().OfType()) { if (notifyTarget == commonParent) break; notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); } - } - if (Captured is Visual v3) - v3.DetachedFromVisualTree += OnCaptureDetached; + if (Captured is Visual newVisual) + newVisual.DetachedFromVisualTree += OnCaptureDetached; if (Captured != null) CaptureGestureRecognizer(null); @@ -92,7 +113,7 @@ namespace Avalonia.Input public IInputElement? Captured { get; private set; } - + public PointerType Type { get; } public bool IsPrimary { get; } diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index bbcc5cccd8..7682b0fb22 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -202,11 +202,26 @@ namespace Avalonia.Input { public IPointer Pointer { get; } - [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] + [Unstable("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.")] public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { Pointer = pointer; Source = source; } } + + internal class PointerCaptureChangingEventArgs : RoutedEventArgs + { + public IPointer Pointer { get; } + public CaptureSource CaptureSource { get; } + public IInputElement? NewValue { get; } + + internal PointerCaptureChangingEventArgs(object source, IPointer pointer, IInputElement? newValue, CaptureSource captureSource) : base(InputElement.PointerCaptureChangingEvent) + { + Pointer = pointer; + Source = source; + NewValue = newValue; + CaptureSource = captureSource; + } + } }