diff --git a/src/Uno.UI/UI/Xaml/Controls/Primitives/SelectorItem.cs b/src/Uno.UI/UI/Xaml/Controls/Primitives/SelectorItem.cs index 876044dd74d6ae235596b76d6c3c197909469f0c..191993d3036f031488bc1d45caac1577b306c9cf 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Primitives/SelectorItem.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Primitives/SelectorItem.cs @@ -7,6 +7,7 @@ using Windows.UI.Xaml.Input; using Windows.UI.Core; using System.Threading.Tasks; using Uno.UI; +using Uno.UI.Xaml.Core; #if XAMARIN_IOS using UIKit; #endif @@ -180,7 +181,7 @@ namespace Windows.UI.Xaml.Controls.Primitives pause = false; } - if (delay < TimeSpan.Zero) + if (delay <= TimeSpan.Zero) { _currentState = state; VisualStateManager.GoToState(this, state, true); diff --git a/src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeControl.Uno.cs b/src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeControl.Uno.cs index 06665100236243c07934632ae8ea35a1c49e76e8..e04d9496818e1cf1bc4b45ef826071dcae724041 100644 --- a/src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeControl.Uno.cs +++ b/src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeControl.Uno.cs @@ -11,6 +11,7 @@ using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Animation; using Uno.UI; +using Uno.UI.Xaml.Core; #if HAS_UNO_WINUI using Microsoft.UI.Input; diff --git a/src/Uno.UI/UI/Xaml/Internal/PointerCapture.cs b/src/Uno.UI/UI/Xaml/Internal/PointerCapture.cs new file mode 100644 index 0000000000000000000000000000000000000000..d080e13bbce7fa34b9d9cc7505f17b545c848753 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/PointerCapture.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Windows.Devices.Input; +using Windows.UI.Xaml.Input; + +using Uno.Extensions; +using Uno.Foundation.Logging; +using Uno.UI.Extensions; +using System.Runtime.CompilerServices; +using Windows.UI.Xaml; + +namespace Uno.UI.Xaml.Core; + +/* + * This file is intrinsically linked to the UIElement and uses some of its internal APIs: + * internal List PointerCapturesBackingField; + * internal partial void CapturePointerNative(Pointer pointer); + * internal partial void ReleasePointerNative(Pointer pointer); + */ + +/// +/// This is an helper class that use to manage the captures for a given pointer. +/// +internal class PointerCapture +{ + private static readonly IDictionary _actives = new Dictionary(EqualityComparer.Default); + + /// + /// Current currently active pointer capture for the given pointer, or creates a new one. + /// + /// The pointer to capture + public static PointerCapture GetOrCreate(Pointer pointer) + => _actives.TryGetValue(pointer.UniqueId, out var capture) + ? capture + : new PointerCapture(pointer); // The capture will be added to the _actives only when a target is added to it. + + internal static bool TryGet(PointerIdentifier pointer, out PointerCapture capture) + => _actives.TryGetValue(pointer, out capture); + + public static bool TryGet(Pointer pointer, out PointerCapture capture) + => _actives.TryGetValue(pointer.UniqueId, out capture); + + public static bool Any(out List cloneOfAllCaptures) + { + if (_actives.Any()) + { + cloneOfAllCaptures = _actives.Values.ToList(); + return true; + } + else + { + cloneOfAllCaptures = default; + return false; + } + } + + private UIElement _nativeCaptureElement; + private readonly Dictionary _targets = new Dictionary(2); + + private PointerCapture(Pointer pointer) + { + Pointer = pointer; + } + + /// + /// The captured pointer + /// + public Pointer Pointer { get; } + + /// + /// Gets the of the last args that has been handled by this capture + /// + public long MostRecentDispatchedEventFrameId { get; private set; } + + /// + /// Determines if this capture was made only for implicit kind + /// (So we should not use it to filter out some event on other controls) + /// + public bool IsImplicitOnly { get; private set; } = true; + + public IEnumerable Targets => _targets.Values; + + internal bool IsTarget(UIElement element, PointerCaptureKind kinds) + => _targets.TryGetValue(element, out var target) + && (target.Kind & kinds) != PointerCaptureKind.None; + + internal IEnumerable GetTargets(PointerCaptureKind kinds) + => _targets + .Values + .Where(target => (target.Kind & kinds) != PointerCaptureKind.None); + + internal PointerCaptureResult TryAddTarget(UIElement element, PointerCaptureKind kind, PointerRoutedEventArgs relatedArgs = null) + { + global::System.Diagnostics.Debug.Assert( + kind == PointerCaptureKind.Explicit || kind == PointerCaptureKind.Implicit, + "The initial capture kind must be Explicit **OR** Implicit."); + + if (this.Log().IsEnabled(LogLevel.Information)) + { + this.Log().Info($"{element.GetDebugName()}: Capturing ({kind}) pointer {Pointer}"); + } + + if (_targets.TryGetValue(element, out var target)) + { + // Validate if the requested kind is not already handled + if (target.Kind.HasFlag(kind)) + { + return PointerCaptureResult.AlreadyCaptured; + } + else + { + // Add the new kind to the target + target.Kind |= kind; + } + } + else + { + target = new PointerCaptureTarget(element, kind); + _targets.Add(element, target); + + // If the capture is made while raising an event (usually captures are made in PointerPressed handlers) + // we re-use the current event args (if they match) to init the target.LastDispatched property. + // Note: we don't check the sender as we may capture on another element but the frame ID is still correct. + if (relatedArgs?.Pointer == Pointer) + { + Update(target, relatedArgs); + + // In case of an implicit capture we also override the native element used for the capture. + // cf. remarks of the PointerCaptureTarget.NativeCaptureElement. + if (kind == PointerCaptureKind.Implicit) + { + target.NativeCaptureElement = relatedArgs?.OriginalSource as UIElement ?? element; + } + } + } + + // If we added an explicit capture, we update the _localExplicitCaptures of the target element + if (kind == PointerCaptureKind.Explicit) + { + IsImplicitOnly = false; + element.PointerCapturesBackingField.Add(Pointer); + } + + // Make sure that this capture is effective + EnsureEffectiveCaptureState(); + + return PointerCaptureResult.Added; + } + + /// + /// Removes a UIElement from the targets of this capture. + /// DO NOT USE directly, use instead the Release method on the UIElement in order to properly raise the PointerCaptureLost event. + /// + internal PointerCaptureKind RemoveTarget(UIElement element, PointerCaptureKind kinds, out PointerRoutedEventArgs lastDispatched) + { + if (!_targets.TryGetValue(element, out var target) + || (target.Kind & kinds) == 0) // Validate if any of the requested kinds is handled + { + lastDispatched = default; + return PointerCaptureKind.None; + } + + var removed = target.Kind & kinds; + lastDispatched = target.LastDispatched; + + RemoveCore(target, kinds); + + return removed; + } + + private void Clear() + { + foreach (var target in _targets.Values.ToList()) + { + RemoveCore(target, PointerCaptureKind.Any); + } + } + + private void RemoveCore(PointerCaptureTarget target, PointerCaptureKind kinds) + { + global::System.Diagnostics.Debug.Assert( + kinds != PointerCaptureKind.None, + "The capture kind must be set to release pointer captures."); + + if (this.Log().IsEnabled(LogLevel.Information)) + { + this.Log().Info($"{target.Element.GetDebugName()}: Releasing ({kinds}) capture of pointer {Pointer}"); + } + + // If we remove an explicit capture, we update the _localExplicitCaptures of the target element + if (kinds.HasFlag(PointerCaptureKind.Explicit) + && target.Kind.HasFlag(PointerCaptureKind.Explicit)) + { + target.Element.PointerCapturesBackingField.Remove(Pointer); + } + + target.Kind &= ~kinds; + + // The element is no longer listening for events, remove it. + if (target.Kind == PointerCaptureKind.None) + { + _targets.Remove(target.Element); + } + + IsImplicitOnly = _targets.None(t => t.Value.Kind.HasFlag(PointerCaptureKind.Explicit)); + + // Validate / update the state of this capture + EnsureEffectiveCaptureState(); + } + + /// + /// Validate if the provided routed event args are relevant for the given element according to the active captures + /// + /// The target element for which the args are validated + /// The pending pointer event args that is under test + /// A flag that allows to automatically release any pending out-dated capture (for PointerDown only) + /// A boolean which indicates if the args are valid or not for the given element + public bool ValidateAndUpdate(UIElement element, PointerRoutedEventArgs args, bool autoRelease) + { + if ((autoRelease && MostRecentDispatchedEventFrameId < args.FrameId) + || _nativeCaptureElement.GetHitTestVisibility() == HitTestability.Collapsed) + { + // If 'autoRelease' we want to release any previous capture that was not release properly no matter the reason. + // BUT we don't want to release a capture that was made by a child control (so MostRecentDispatchedEventFrameId should already be equals to current FrameId). + // We also do not allow a control that is not loaded to keep a capture (they should all have been release on unload). + // ** This is an IMPORTANT safety catch to prevent the application to become unresponsive ** + Clear(); + + return true; + } + else if (_targets.TryGetValue(element, out var target)) + { + Update(target, args); + + return true; + } + else if (IsImplicitOnly) + { + // If the capture is implicit, we should not filter out events for children elements. + + return true; + } + else + { + // We should dispatch the event only if the control which has captured the pointer has already dispatched the event + // (Which actually means that the current control is a parent of the control which has captured the pointer) + // Remarks: This is not enough to determine parent-child relationship when we dispatch multiple events base on the same native event, + // (as they will all have the same FrameId), however in that case we dispatch events layer per layer + // instead of bubbling a single event before raising the next one, so we are safe. + // The only limitation would be when mixing native vs. managed bubbling, but this check only prevents + // the leaf of the tree to raise the event, so we cannot mix bubbling mode in that case. + return MostRecentDispatchedEventFrameId >= args.FrameId; + } + } + + private void Update(PointerCaptureTarget target, PointerRoutedEventArgs args) + { + target.LastDispatched = args; + if (MostRecentDispatchedEventFrameId < args.FrameId) + { + MostRecentDispatchedEventFrameId = args.FrameId; + } + } + + private void EnsureEffectiveCaptureState() + { + if (_targets.Any()) + { + // We have some target, self enable us + + if (_actives.TryGetValue(Pointer.UniqueId, out var capture)) + { + if (capture != this) + { + throw new InvalidOperationException("There is already another active capture."); + } + } + else + { + // This is what makes this capture active + _actives.Add(Pointer.UniqueId, this); + } + + if (_nativeCaptureElement == null) + { + _nativeCaptureElement = _targets.Single().Value.NativeCaptureElement; + + CapturePointerNative(); + } + } + else + { + // We no longer have any target, cleanup + + if (_nativeCaptureElement != null) + { + ReleasePointerNative(); + + _nativeCaptureElement = null; + } + + if (_actives.TryGetValue(Pointer.UniqueId, out var capture) && capture == this) + { + // This is what makes this capture inactive + _actives.Remove(Pointer.UniqueId); + } + } + } + + /// + /// This method contains or is called by a try/catch containing method and can + /// be significantly slower than other methods as a result on WebAssembly. + /// See https://github.com/dotnet/runtime/issues/56309 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReleasePointerNative() + { + try + { + _nativeCaptureElement.ReleasePointerNative(Pointer); + } + catch (Exception e) + { + this.Log().Error($"Failed to release native capture of {Pointer}", e); + } + } + + /// + /// This method contains or is called by a try/catch containing method and + /// can be significantly slower than other methods as a result on WebAssembly. + /// See https://github.com/dotnet/runtime/issues/56309 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CapturePointerNative() + { + try + { + _nativeCaptureElement.CapturePointerNative(Pointer); + } + catch (Exception e) + { + this.Log().Error($"Failed to capture natively pointer {Pointer}.", e); + } + } +} diff --git a/src/Uno.UI/UI/Xaml/Internal/PointerCaptureKind.cs b/src/Uno.UI/UI/Xaml/Internal/PointerCaptureKind.cs new file mode 100644 index 0000000000000000000000000000000000000000..e610f1c08e2ccb2cf65e1309cbebe91ae6104f33 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/PointerCaptureKind.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; + +namespace Uno.UI.Xaml.Core; + +[Flags] +internal enum PointerCaptureKind : byte +{ + None = 0, + + Explicit = 1, + Implicit = 2, + + Any = Explicit | Implicit, +} diff --git a/src/Uno.UI/UI/Xaml/Internal/PointerCaptureResult.cs b/src/Uno.UI/UI/Xaml/Internal/PointerCaptureResult.cs new file mode 100644 index 0000000000000000000000000000000000000000..958c5c18650d14e9076f2efb2703f22803077e4b --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/PointerCaptureResult.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; + +namespace Uno.UI.Xaml.Core; + +internal enum PointerCaptureResult +{ + /// + /// The capture has been added for the given element. + /// + Added, + + /// + /// The pointer has already been captured with the same kind by the given element. + /// + AlreadyCaptured, + + /// + /// The pointer has already been captured by another element, + /// or it cannot be captured at this time (pointer not pressed). + /// + Failed, +} diff --git a/src/Uno.UI/UI/Xaml/Internal/PointerCaptureTarget.cs b/src/Uno.UI/UI/Xaml/Internal/PointerCaptureTarget.cs new file mode 100644 index 0000000000000000000000000000000000000000..1334de2a06b41e6e668bbf70bf3737f581f6f0c5 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Internal/PointerCaptureTarget.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Input; + +namespace Uno.UI.Xaml.Core; + +internal class PointerCaptureTarget +{ + internal PointerCaptureTarget(UIElement element, PointerCaptureKind kind) + { + NativeCaptureElement = Element = element; + Kind = kind; + } + + /// + /// The target element to which event args should be forwarded + /// + public UIElement Element { get; } + + /// + /// The element to used for the native capture + /// + /// + /// On WASM this might be different than the : + /// In case of implicit capture, the element used for the capture will prevent any pointer event on sub element + /// (sub element will actually get a pointer 'leave' on capture, and a 'enter' on capture release). + /// So instead of capturing using the actual element, we use the 'OriginalSource' of the 'relatedArgs', + /// so event will still be sent to sub elements and we will then filter them out if needed. + /// + public UIElement NativeCaptureElement { get; set; } + + /// + /// Gets the current capture kinds that was enabled on the target + /// + internal PointerCaptureKind Kind { get; set; } + + /// + /// Determines if the is in the native bubbling tree. + /// If so we could rely on standard events bubbling to reach it. + /// Otherwise this means that we have to bubble the event in managed only. + /// + /// This makes sense only for platform that has "implicit capture" + /// (i.e. all pointers events are sent to the element on which the pointer pressed + /// occured at the beginning of the gesture). This is the case on iOS and Android. + /// + public bool? IsInNativeBubblingTree { get; set; } + + /// + /// Gets the last event dispatched by the . + /// In case of native bubbling (cf. ), + /// this helps to determine that an event was already dispatched by the Owner: + /// if a UIElement is receiving and event with the same timestamp, it means that the element + /// is a parent of the Owner and we are only bubbling the routed event, so this element can + /// raise the event (if the opposite, it means that the element is a child, so it has to mute the event). + /// + public PointerRoutedEventArgs LastDispatched { get; set; } +} diff --git a/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs b/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs deleted file mode 100644 index 5ae64062db1d3d80df917e9c5d54a3971b079b57..0000000000000000000000000000000000000000 --- a/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs +++ /dev/null @@ -1,430 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Windows.Devices.Input; -using Windows.UI.Xaml.Input; - -using Uno.Extensions; -using Uno.Foundation.Logging; -using Uno.UI.Extensions; -using System.Runtime.CompilerServices; - -namespace Windows.UI.Xaml -{ - partial class UIElement - { - /* - * This partial file contains only the helper classes for the pointer capture handling which is located in the "Pointers" file. - * Those classes are in UIElement only to get access to the partial native capture APIs. - * - * This file does not implements any partial API, but is using: - * partial void CapturePointerNative(Pointer pointer); - * partial void ReleasePointerNative(Pointer pointer); - */ - - [Flags] - internal enum PointerCaptureKind : byte - { - None = 0, - - Explicit = 1, - Implicit = 2, - - Any = Explicit | Implicit, - } - - internal enum PointerCaptureResult - { - /// - /// The capture has been added for the given element. - /// - Added, - - /// - /// The pointer has already been captured with the same kind by the given element. - /// - AlreadyCaptured, - - /// - /// The pointer has already been captured by another element, - /// or it cannot be captured at this time (pointer not pressed). - /// - Failed, - } - - internal protected class PointerCapture - { - private static readonly IDictionary _actives = new Dictionary(EqualityComparer.Default); - - /// - /// Current currently active pointer capture for the given pointer, or creates a new one. - /// - /// The pointer to capture - public static PointerCapture GetOrCreate(Pointer pointer) - => _actives.TryGetValue(pointer.UniqueId, out var capture) - ? capture - : new PointerCapture(pointer); // The capture will be added to the _actives only when a target is added to it. - - internal static bool TryGet(PointerIdentifier pointer, out PointerCapture capture) - => _actives.TryGetValue(pointer, out capture); - - public static bool TryGet(Pointer pointer, out PointerCapture capture) - => _actives.TryGetValue(pointer.UniqueId, out capture); - - public static bool Any(out List cloneOfAllCaptures) - { - if (_actives.Any()) - { - cloneOfAllCaptures = _actives.Values.ToList(); - return true; - } - else - { - cloneOfAllCaptures = default; - return false; - } - } - - private UIElement _nativeCaptureElement; - private readonly Dictionary _targets = new Dictionary(2); - - private PointerCapture(Pointer pointer) - { - Pointer = pointer; - } - - /// - /// The captured pointer - /// - public Pointer Pointer { get; } - - /// - /// Gets the of the last args that has been handled by this capture - /// - public long MostRecentDispatchedEventFrameId { get; private set; } - - /// - /// Determines if this capture was made only for implicit kind - /// (So we should not use it to filter out some event on other controls) - /// - public bool IsImplicitOnly { get; private set; } = true; - - public IEnumerable Targets => _targets.Values; - - internal bool IsTarget(UIElement element, PointerCaptureKind kinds) - => _targets.TryGetValue(element, out var target) - && (target.Kind & kinds) != PointerCaptureKind.None; - - internal IEnumerable GetTargets(PointerCaptureKind kinds) - => _targets - .Values - .Where(target => (target.Kind & kinds) != PointerCaptureKind.None); - - internal PointerCaptureResult TryAddTarget(UIElement element, PointerCaptureKind kind, PointerRoutedEventArgs relatedArgs = null) - { - global::System.Diagnostics.Debug.Assert( - kind == PointerCaptureKind.Explicit || kind == PointerCaptureKind.Implicit, - "The initial capture kind must be Explicit **OR** Implicit."); - - if (this.Log().IsEnabled(LogLevel.Information)) - { - this.Log().Info($"{element}: Capturing ({kind}) pointer {Pointer}"); - } - - if (_targets.TryGetValue(element, out var target)) - { - // Validate if the requested kind is not already handled - if (target.Kind.HasFlag(kind)) - { - return PointerCaptureResult.AlreadyCaptured; - } - else - { - // Add the new kind to the target - target.Kind |= kind; - } - } - else - { - target = new PointerCaptureTarget(element, kind); - _targets.Add(element, target); - - // If the capture is made while raising an event (usually captures are made in PointerPressed handlers) - // we re-use the current event args (if they match) to init the target.LastDispatched property. - // Note: we don't check the sender as we may capture on another element but the frame ID is still correct. - if (relatedArgs?.Pointer == Pointer) - { - Update(target, relatedArgs); - - // In case of an implicit capture we also override the native element used for the capture. - // cf. remarks of the PointerCaptureTarget.NativeCaptureElement. - if (kind == PointerCaptureKind.Implicit) - { - target.NativeCaptureElement = relatedArgs?.OriginalSource as UIElement ?? element; - } - } - } - - // If we added an explicit capture, we update the _localExplicitCaptures of the target element - if (kind == PointerCaptureKind.Explicit) - { - IsImplicitOnly = false; - element._localExplicitCaptures.Add(Pointer); - } - - // Make sure that this capture is effective - EnsureEffectiveCaptureState(); - - return PointerCaptureResult.Added; - } - - /// - /// Removes a UIElement from the targets of this capture. - /// DO NOT USE directly, use instead the Release method on the UIElement in order to properly raise the PointerCaptureLost event. - /// - internal PointerCaptureKind RemoveTarget(UIElement element, PointerCaptureKind kinds, out PointerRoutedEventArgs lastDispatched) - { - if (!_targets.TryGetValue(element, out var target) - || (target.Kind & kinds) == 0) // Validate if any of the requested kinds is handled - { - lastDispatched = default; - return PointerCaptureKind.None; - } - - var removed = target.Kind & kinds; - lastDispatched = target.LastDispatched; - - RemoveCore(target, kinds); - - return removed; - } - - private void Clear() - { - foreach (var target in _targets.Values.ToList()) - { - RemoveCore(target, PointerCaptureKind.Any); - } - } - - private void RemoveCore(PointerCaptureTarget target, PointerCaptureKind kinds) - { - global::System.Diagnostics.Debug.Assert( - kinds != PointerCaptureKind.None, - "The capture kind must be set to release pointer captures."); - - if (this.Log().IsEnabled(LogLevel.Information)) - { - this.Log().Info($"{target.Element.GetDebugName()}: Releasing ({kinds}) capture of pointer {Pointer}"); - } - - // If we remove an explicit capture, we update the _localExplicitCaptures of the target element - if (kinds.HasFlag(PointerCaptureKind.Explicit) - && target.Kind.HasFlag(PointerCaptureKind.Explicit)) - { - target.Element._localExplicitCaptures.Remove(Pointer); - } - - target.Kind &= ~kinds; - - // The element is no longer listening for events, remove it. - if (target.Kind == PointerCaptureKind.None) - { - _targets.Remove(target.Element); - } - - IsImplicitOnly = _targets.None(t => t.Value.Kind.HasFlag(PointerCaptureKind.Explicit)); - - // Validate / update the state of this capture - EnsureEffectiveCaptureState(); - } - - /// - /// Validate if the provided routed event args are relevant for the given element according to the active captures - /// - /// The target element for which the args are validated - /// The pending pointer event args that is under test - /// A flag that allows to automatically release any pending out-dated capture (for PointerDown only) - /// A boolean which indicates if the args are valid or not for the given element - public bool ValidateAndUpdate(UIElement element, PointerRoutedEventArgs args, bool autoRelease) - { - if ((autoRelease && MostRecentDispatchedEventFrameId < args.FrameId) - || _nativeCaptureElement.GetHitTestVisibility() == HitTestability.Collapsed) - { - // If 'autoRelease' we want to release any previous capture that was not release properly no matter the reason. - // BUT we don't want to release a capture that was made by a child control (so MostRecentDispatchedEventFrameId should already be equals to current FrameId). - // We also do not allow a control that is not loaded to keep a capture (they should all have been release on unload). - // ** This is an IMPORTANT safety catch to prevent the application to become unresponsive ** - Clear(); - - return true; - } - else if (_targets.TryGetValue(element, out var target)) - { - Update(target, args); - - return true; - } - else if (IsImplicitOnly) - { - // If the capture is implicit, we should not filter out events for children elements. - - return true; - } - else - { - // We should dispatch the event only if the control which has captured the pointer has already dispatched the event - // (Which actually means that the current control is a parent of the control which has captured the pointer) - // Remarks: This is not enough to determine parent-child relationship when we dispatch multiple events base on the same native event, - // (as they will all have the same FrameId), however in that case we dispatch events layer per layer - // instead of bubbling a single event before raising the next one, so we are safe. - // The only limitation would be when mixing native vs. managed bubbling, but this check only prevents - // the leaf of the tree to raise the event, so we cannot mix bubbling mode in that case. - return MostRecentDispatchedEventFrameId >= args.FrameId; - } - } - - private void Update(PointerCaptureTarget target, PointerRoutedEventArgs args) - { - target.LastDispatched = args; - if (MostRecentDispatchedEventFrameId < args.FrameId) - { - MostRecentDispatchedEventFrameId = args.FrameId; - } - } - - private void EnsureEffectiveCaptureState() - { - if (_targets.Any()) - { - // We have some target, self enable us - - if (_actives.TryGetValue(Pointer.UniqueId, out var capture)) - { - if (capture != this) - { - throw new InvalidOperationException("There is already another active capture."); - } - } - else - { - // This is what makes this capture active - _actives.Add(Pointer.UniqueId, this); - } - - if (_nativeCaptureElement == null) - { - _nativeCaptureElement = _targets.Single().Value.NativeCaptureElement; - - CapturePointerNative(); - } - } - else - { - // We no longer have any target, cleanup - - if (_nativeCaptureElement != null) - { - ReleasePointerNative(); - - _nativeCaptureElement = null; - } - - if (_actives.TryGetValue(Pointer.UniqueId, out var capture) && capture == this) - { - // This is what makes this capture inactive - _actives.Remove(Pointer.UniqueId); - } - } - } - - /// - /// This method contains or is called by a try/catch containing method and can - /// be significantly slower than other methods as a result on WebAssembly. - /// See https://github.com/dotnet/runtime/issues/56309 - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReleasePointerNative() - { - try - { - _nativeCaptureElement.ReleasePointerNative(Pointer); - } - catch (Exception e) - { - this.Log().Error($"Failed to release native capture of {Pointer}", e); - } - } - - /// - /// This method contains or is called by a try/catch containing method and - /// can be significantly slower than other methods as a result on WebAssembly. - /// See https://github.com/dotnet/runtime/issues/56309 - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CapturePointerNative() - { - try - { - _nativeCaptureElement.CapturePointerNative(Pointer); - } - catch (Exception e) - { - this.Log().Error($"Failed to capture natively pointer {Pointer}.", e); - } - } - } - - internal protected class PointerCaptureTarget - { - internal PointerCaptureTarget(UIElement element, PointerCaptureKind kind) - { - NativeCaptureElement = Element = element; - Kind = kind; - } - - /// - /// The target element to which event args should be forwarded - /// - public UIElement Element { get; } - - /// - /// The element to used for the native capture - /// - /// - /// On WASM this might be different than the : - /// In case of implicit capture, the element used for the capture will prevent any pointer event on sub element - /// (sub element will actually get a pointer 'leave' on capture, and a 'enter' on capture release). - /// So instead of capturing using the actual element, we use the 'OriginalSource' of the 'relatedArgs', - /// so event will still be sent to sub elements and we will then filter them out if needed. - /// - public UIElement NativeCaptureElement { get; set; } - - /// - /// Gets tha current capture kind that was enabled on the target - /// - internal PointerCaptureKind Kind { get; set; } - - /// - /// Determines if the is in the native bubbling tree. - /// If so we could rely on standard events bubbling to reach it. - /// Otherwise this means that we have to bubble the event in managed only. - /// - /// This makes sense only for platform that has "implicit capture" - /// (i.e. all pointers events are sent to the element on which the pointer pressed - /// occured at the beginning of the gesture). This is the case on iOS and Android. - /// - public bool? IsInNativeBubblingTree { get; set; } - - /// - /// Gets the last event dispatched by the . - /// In case of native bubbling (cf. ), - /// this helps to determine that an event was already dispatched by the Owner: - /// if a UIElement is receiving and event with the same timestamp, it means that the element - /// is a parent of the Owner and we are only bubbling the routed event, so this element can - /// raise the event (if the opposite, it means that the element is a child, so it has to mute the event). - /// - public PointerRoutedEventArgs LastDispatched { get; set; } - } - } -} diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs index 62c3b752f0f68277dab2352a6d68806464bd3b2c..b1eeec12c64ad25af2fc1a18785afd6f3d0bb99f 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs @@ -104,10 +104,10 @@ namespace Windows.UI.Xaml #endregion - partial void CapturePointerNative(Pointer pointer) + internal partial void CapturePointerNative(Pointer pointer) => XamlRoot?.VisualTree.ContentRoot.InputManager!.SetPointerCapture(pointer.UniqueId); - partial void ReleasePointerNative(Pointer pointer) + internal partial void ReleasePointerNative(Pointer pointer) => XamlRoot?.VisualTree.ContentRoot.InputManager!.ReleasePointerCapture(pointer.UniqueId); } } diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs index d566aa29b73030a05c6ab5e87d2cee75dbc21354..14ace5552d94632d941e7c8b7b3a4b2a47afade7 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs @@ -1267,7 +1267,7 @@ namespace Windows.UI.Xaml /// internal bool IsPressed(Pointer pointer) => _pressedPointers.Contains(pointer.PointerId); - private bool IsPressed(uint pointerId) => _pressedPointers.Contains(pointerId); + internal bool IsPressed(uint pointerId) => _pressedPointers.Contains(pointerId); private bool SetPressed(PointerRoutedEventArgs args, bool isPressed, BubblingContext ctx) { @@ -1321,7 +1321,13 @@ namespace Windows.UI.Xaml * - The PointersCapture property remains `null` until a pointer is captured */ - private List _localExplicitCaptures; + /// + /// DO NOT USE + /// This is the backing field of . + /// It's internal to be accessible to the and must not be used directly by any other code! + /// Use dedicated APIs like , and instead. + /// + internal List PointerCapturesBackingField; #region Capture public (and internal) API ==> This manages only Explicit captures public static DependencyProperty PointerCapturesProperty { get; } = DependencyProperty.Register( @@ -1336,7 +1342,7 @@ namespace Windows.UI.Xaml /// Indicates if this UIElement has any active ** EXPLICIT ** pointer capture. /// #if __ANDROID__ - internal new bool HasPointerCapture => (_localExplicitCaptures?.Count ?? 0) != 0; + internal new bool HasPointerCapture => (PointerCapturesBackingField?.Count ?? 0) != 0; #else internal bool HasPointerCapture => (_localExplicitCaptures?.Count ?? 0) != 0; #endif @@ -1403,8 +1409,8 @@ namespace Windows.UI.Xaml } #endregion - partial void CapturePointerNative(Pointer pointer); - partial void ReleasePointerNative(Pointer pointer); + internal partial void CapturePointerNative(Pointer pointer); + internal partial void ReleasePointerNative(Pointer pointer); private bool ValidateAndUpdateCapture(PointerRoutedEventArgs args) => ValidateAndUpdateCapture(args, IsOver(args.Pointer)); @@ -1454,10 +1460,10 @@ namespace Windows.UI.Xaml private PointerCaptureResult Capture(Pointer pointer, PointerCaptureKind kind, PointerRoutedEventArgs relatedArgs) { - if (_localExplicitCaptures == null) + if (PointerCapturesBackingField == null) { - _localExplicitCaptures = new List(); - this.SetValue(PointerCapturesProperty, _localExplicitCaptures); // Note: On UWP this is done only on first capture (like here) + PointerCapturesBackingField = new List(); + this.SetValue(PointerCapturesProperty, PointerCapturesBackingField); // Note: On UWP this is done only on first capture (like here) } return PointerCapture.GetOrCreate(pointer).TryAddTarget(this, kind, relatedArgs);