From f7f9cf04e6d7b84c18a31db614dac6227eb93f1b Mon Sep 17 00:00:00 2001 From: David Date: Wed, 5 May 2021 17:15:00 -0400 Subject: [PATCH] fix(pointers): [Skia] Fix some pointers event not being sent to nested elements when pointer implictly captured --- .../UI/Xaml/UIElement.PointerCapture.cs | 2 +- .../UI/Xaml/UIElement.Pointers.Managed.cs | 170 ++++++++---------- src/Uno.UI/UI/Xaml/UIElement.Pointers.cs | 91 +++++----- src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs | 54 +++++- 4 files changed, 169 insertions(+), 148 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs b/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs index bd63f02d0cdd..62bb5260bf50 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs @@ -230,7 +230,7 @@ public bool ValidateAndUpdate(UIElement element, PointerRoutedEventArgs args, bo || _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 LastDispatchedEventFrameId should already be equals to current FrameId). + // 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(); diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs index 4c5a3c61ff58..469e6da70b97 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs @@ -70,17 +70,7 @@ private void CoreWindow_PointerWheelChanged(CoreWindow sender, PointerEventArgs var routedArgs = new PointerRoutedEventArgs(args, originalSource); // Second raise the event, either on the OriginalSource or on the capture owners if any - if (PointerCapture.TryGet(routedArgs.Pointer, out var capture)) - { - foreach (var target in capture.Targets.ToArray()) - { - target.Element.OnNativePointerWheel(routedArgs); - } - } - else - { - originalSource.OnNativePointerWheel(routedArgs); - } + RaiseUsingCaptures(Wheel, originalSource, routedArgs); } private void CoreWindow_PointerEntered(CoreWindow sender, PointerEventArgs args) @@ -109,7 +99,7 @@ private void CoreWindow_PointerEntered(CoreWindow sender, PointerEventArgs args) var routedArgs = new PointerRoutedEventArgs(args, originalSource); - originalSource.OnNativePointerEnter(routedArgs); + Raise(Enter, originalSource, routedArgs); } private void CoreWindow_PointerExited(CoreWindow sender, PointerEventArgs args) @@ -135,7 +125,7 @@ private void CoreWindow_PointerExited(CoreWindow sender, PointerEventArgs args) var routedArgs = new PointerRoutedEventArgs(args, originalSource); - ClearPointerState(routedArgs, null, overBranchLeaf); + Raise(Leave, overBranchLeaf, routedArgs); } private void CoreWindow_PointerPressed(CoreWindow sender, PointerEventArgs args) @@ -165,7 +155,7 @@ private void CoreWindow_PointerPressed(CoreWindow sender, PointerEventArgs args) var routedArgs = new PointerRoutedEventArgs(args, originalSource); _pressedElements[routedArgs.Pointer] = originalSource; - originalSource.OnNativePointerDown(routedArgs); + Raise(Pressed, originalSource, routedArgs); } private void CoreWindow_PointerReleased(CoreWindow sender, PointerEventArgs args) @@ -194,31 +184,10 @@ private void CoreWindow_PointerReleased(CoreWindow sender, PointerEventArgs args var routedArgs = new PointerRoutedEventArgs(args, originalSource); - // Second raise the event, either on the OriginalSource or on the capture owners if any - if (PointerCapture.TryGet(routedArgs.Pointer, out var capture)) - { - foreach (var target in capture.Targets.ToArray()) - { - target.Element.OnNativePointerUp(routedArgs); - } - } - else - { - originalSource.OnNativePointerUp(routedArgs); - } - - if (_pressedElements.TryGetValue(routedArgs.Pointer, out var pressedLeaf)) - { - // We must make sure to clear the pressed state on all elements that was flagged as pressed. - // This is required as the current originalSource might not be the same as when we pressed (pointer moved), - // ** OR ** the pointer has been captured by a parent element so we didn't raised to released on the sub elements. - - _pressedElements.Remove(routedArgs.Pointer); - ClearPointerState(routedArgs, root: null, pressedLeaf, clearOver: false); - } + RaiseUsingCaptures(Released, originalSource, routedArgs); + ClearPressedState(routedArgs); } - private void CoreWindow_PointerMoved(CoreWindow sender, PointerEventArgs args) { var (originalSource, staleBranch) = VisualTreeHelper.HitTest(args.CurrentPoint.Position, isStale: _isOver); @@ -247,31 +216,17 @@ private void CoreWindow_PointerMoved(CoreWindow sender, PointerEventArgs args) // First raise the PointerExited events on the stale branch if (staleBranch.HasValue) { - var (root, leaf) = staleBranch.Value; - - ClearPointerState(routedArgs, root, leaf); + Raise(Leave, staleBranch.Value, routedArgs); } // Second (try to) raise the PointerEnter on the OriginalSource // Note: This won't do anything if already over. routedArgs.Handled = false; - originalSource.OnNativePointerEnter(routedArgs); + Raise(Enter, originalSource, routedArgs); // Finally raise the event, either on the OriginalSource or on the capture owners if any - if (PointerCapture.TryGet(routedArgs.Pointer, out var capture)) - { - foreach (var target in capture.Targets.ToArray()) - { - routedArgs.Handled = false; - target.Element.OnNativePointerMove(routedArgs); - } - } - else - { - // Note: We prefer to use the "WithOverCheck" overload as we already know that the pointer is effectively over - routedArgs.Handled = false; - originalSource.OnNativePointerMoveWithOverCheck(routedArgs, isOver: true); - } + routedArgs.Handled = false; + RaiseUsingCaptures(Move, originalSource, routedArgs); } private void CoreWindow_PointerCancelled(CoreWindow sender, PointerEventArgs args) @@ -299,19 +254,12 @@ private void CoreWindow_PointerCancelled(CoreWindow sender, PointerEventArgs arg var routedArgs = new PointerRoutedEventArgs(args, originalSource); - // Second raise the event, either on the OriginalSource or on the capture owners if any - if (PointerCapture.TryGet(routedArgs.Pointer, out var capture)) - { - foreach (var target in capture.Targets.ToArray()) - { - target.Element.OnNativePointerCancel(routedArgs, isSwallowedBySystem: false); - } - } - else - { - originalSource.OnNativePointerCancel(routedArgs, isSwallowedBySystem: false); - } + RaiseUsingCaptures(Cancelled, originalSource, routedArgs); + ClearPressedState(routedArgs); + } + private void ClearPressedState(PointerRoutedEventArgs routedArgs) + { if (_pressedElements.TryGetValue(routedArgs.Pointer, out var pressedLeaf)) { // We must make sure to clear the pressed state on all elements that was flagged as pressed. @@ -319,42 +267,80 @@ private void CoreWindow_PointerCancelled(CoreWindow sender, PointerEventArgs arg // ** OR ** the pointer has been captured by a parent element so we didn't raised to released on the sub elements. _pressedElements.Remove(routedArgs.Pointer); - ClearPointerState(routedArgs, root: null, pressedLeaf, clearOver: false); + + // Note: The event is propagated silently (public events won't be raised) as it's only to clear internal state + var ctx = new BubblingContext {IsInternal = true}; + pressedLeaf.OnPointerUp(routedArgs, ctx); } } - // Clears the pointer state (over and pressed) only for a part of the visual tree - private void ClearPointerState(PointerRoutedEventArgs routedArgs, UIElement? root, UIElement leaf, bool clearOver = true) - { - var element = leaf; + #region Helpers + private delegate void RaisePointerEventArgs(UIElement element, PointerRoutedEventArgs args, BubblingContext ctx); - routedArgs.CanBubbleNatively = true; // TODO: UGLY HACK TO AVOID BUBBLING: we should be able to request to bubble only up to a the root - if (this.Log().IsEnabled(LogLevel.Trace)) - { - this.Log().Trace($"Exiting branch from (root) {root.GetDebugName()} to (leaf) {element.GetDebugName()}\r\n"); - } - - while (element is { }) + private static readonly RaisePointerEventArgs Wheel = (elt, args, ctx) => elt.OnPointerWheel(args, ctx); + private static readonly RaisePointerEventArgs Enter = (elt, args, ctx) => elt.OnPointerEnter(args, ctx); + private static readonly RaisePointerEventArgs Leave = (elt, args, ctx) => + { + elt.OnPointerExited(args, ctx); + + // Even if it's not true, when pointer is leaving an element, we propagate a SILENT (a.k.a. internal) up event to clear the pressed state. + // Note: This is usually limited only to a given branch (cf. Move) + // Note: This differs of how we behave on iOS, macOS and Android which does have "implicit capture" while pressed. + // It should only impact the "Pressed" visual states of controls. + ctx.IsInternal = true; + args.Handled = false; + elt.OnPointerUp(args, ctx); + }; + private static readonly RaisePointerEventArgs Pressed = (elt, args, ctx) => elt.OnPointerDown(args, ctx); + private static readonly RaisePointerEventArgs Released = (elt, args, ctx) => elt.OnPointerUp(args, ctx); + private static readonly RaisePointerEventArgs Move = (elt, args, ctx) => elt.OnPointerMove(args, ctx); + private static readonly RaisePointerEventArgs Cancelled = (elt, args, ctx) => elt.OnPointerCancel(args, ctx); + + private static void Raise(RaisePointerEventArgs raise, UIElement originalSource, PointerRoutedEventArgs routedArgs) + => raise(originalSource, routedArgs, BubblingContext.Bubble); + + private static void Raise(RaisePointerEventArgs raise, VisualTreeHelper.Branch branch, PointerRoutedEventArgs routedArgs) + => raise(branch.Leaf, routedArgs, BubblingContext.BubbleUpTo(branch.Root)); + + private static void RaiseUsingCaptures(RaisePointerEventArgs raise, UIElement originalSource, PointerRoutedEventArgs routedArgs) + { + if (PointerCapture.TryGet(routedArgs.Pointer, out var capture)) { - routedArgs.Handled = false; - if (clearOver) + var targets = capture.Targets.ToList(); + if (capture.IsImplicitOnly) { - element.OnNativePointerExited(routedArgs); - } - // TODO: This differs of how we behave on iOS, macOS and Android which does have "implicit capture" while pressed. - // It should only impact the "Pressed" visual states of controls. - element.SetPressed(routedArgs, isPressed: false, muteEvent: true); + raise(originalSource, routedArgs, BubblingContext.Bubble); - if (element == root) - { - break; + foreach (var target in targets) + { + routedArgs.Handled = false; + raise(target.Element, routedArgs, BubblingContext.NoBubbling); + } } + else + { + var explicitTarget = targets.Find(c => c.Kind == PointerCaptureKind.Explicit)!; - element = element.GetParent() as UIElement; - } + raise(explicitTarget.Element, routedArgs, BubblingContext.Bubble); - routedArgs.CanBubbleNatively = false; + foreach (var target in targets) + { + if (target == explicitTarget) + { + continue; + } + + routedArgs.Handled = false; + raise(target.Element, routedArgs, BubblingContext.NoBubbling); + } + } + } + else + { + raise(originalSource, routedArgs, BubblingContext.Bubble); + } } + #endregion } // TODO Should be per CoreWindow diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs index 09533ba67178..1b65d8619ad1 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs @@ -485,7 +485,7 @@ partial void PrepareManagedDragAndDropEventBubbling(RoutedEvent routedEvent, ref case RoutedEventFlag.DragStarting: case RoutedEventFlag.DropCompleted: // Those are actually not routed events :O - bubblingMode = BubblingMode.StopBubbling; + bubblingMode = BubblingMode.NoBubbling; break; case RoutedEventFlag.DragEnter: @@ -661,22 +661,22 @@ partial void PrepareManagedPointerEventBubbling(RoutedEvent routedEvent, ref Rou switch (routedEvent.Flag) { case RoutedEventFlag.PointerEntered: - OnManagedPointerEnter(ptArgs); + OnPointerEnter(ptArgs, BubblingContext.OnManagedBubbling); break; case RoutedEventFlag.PointerPressed: - OnManagedPointerDown(ptArgs); + OnPointerDown(ptArgs, BubblingContext.OnManagedBubbling); break; case RoutedEventFlag.PointerMoved: - OnManagePointerMove(ptArgs); + OnPointerMove(ptArgs, BubblingContext.OnManagedBubbling); break; case RoutedEventFlag.PointerReleased: - OnManagedPointerUp(ptArgs); + OnPointerUp(ptArgs, BubblingContext.OnManagedBubbling); break; case RoutedEventFlag.PointerExited: - OnManagedPointerExited(ptArgs); + OnPointerExited(ptArgs, BubblingContext.OnManagedBubbling); break; case RoutedEventFlag.PointerCanceled: - OnManagedPointerCancel(ptArgs); + OnPointerCancel(ptArgs, BubblingContext.OnManagedBubbling); break; // No local state (over/pressed/manipulation/gestures) to update for // - PointerCaptureLost: @@ -685,22 +685,20 @@ partial void PrepareManagedPointerEventBubbling(RoutedEvent routedEvent, ref Rou } #region Partial API to raise pointer events and gesture recognition (OnNative***) - private bool OnNativePointerEnter(PointerRoutedEventArgs args) => OnPointerEnter(args, isManagedBubblingEvent: false); - private void OnManagedPointerEnter(PointerRoutedEventArgs args) => OnPointerEnter(args, isManagedBubblingEvent: true); + private bool OnNativePointerEnter(PointerRoutedEventArgs args, BubblingContext ctx = default) => OnPointerEnter(args); - private bool OnPointerEnter(PointerRoutedEventArgs args, bool isManagedBubblingEvent) + private bool OnPointerEnter(PointerRoutedEventArgs args, BubblingContext ctx = default) { // We override the isOver for the relevancy check as we will update it right after. var isOverOrCaptured = ValidateAndUpdateCapture(args, isOver: true); - var handledInManaged = SetOver(args, true, muteEvent: isManagedBubblingEvent || !isOverOrCaptured); + var handledInManaged = SetOver(args, true, muteEvent: ctx.IsLocalOnly || !isOverOrCaptured); return handledInManaged; } - private bool OnNativePointerDown(PointerRoutedEventArgs args) => OnPointerDown(args, isManagedBubblingEvent: false); - private void OnManagedPointerDown(PointerRoutedEventArgs args) => OnPointerDown(args, isManagedBubblingEvent: true); + private bool OnNativePointerDown(PointerRoutedEventArgs args) => OnPointerDown(args); - private bool OnPointerDown(PointerRoutedEventArgs args, bool isManagedBubblingEvent) + private bool OnPointerDown(PointerRoutedEventArgs args, BubblingContext ctx = default) { _isGestureCompleted = false; @@ -708,9 +706,9 @@ private bool OnPointerDown(PointerRoutedEventArgs args, bool isManagedBubblingEv // it due to an invalid state. So here we make sure to not stay in an invalid state that would // prevent any interaction with the application. var isOverOrCaptured = ValidateAndUpdateCapture(args, isOver: true, forceRelease: true); - var handledInManaged = SetPressed(args, true, muteEvent: isManagedBubblingEvent || !isOverOrCaptured); + var handledInManaged = SetPressed(args, true, muteEvent: ctx.IsLocalOnly || !isOverOrCaptured); - if (PointerRoutedEventArgs.PlatformSupportsNativeBubbling && !isManagedBubblingEvent && !isOverOrCaptured) + if (PointerRoutedEventArgs.PlatformSupportsNativeBubbling && !ctx.IsLocalOnly && !isOverOrCaptured) { // This case is for safety only, it should not happen as we should never get a Pointer down while not // on this UIElement, and no capture should prevent the dispatch as no parent should hold a capture at this point. @@ -745,7 +743,8 @@ private bool OnPointerDown(PointerRoutedEventArgs args, bool isManagedBubblingEv return handledInManaged; } - // This is for iOS and Android which not raising the Exit properly and for which we have to re-compute the over state for each move + // This is for iOS and Android which are not raising the Exit properly (due to native "implicit capture" when pointer is pressed), + // and for which we have to re-compute / update the over state for each move. private bool OnNativePointerMoveWithOverCheck(PointerRoutedEventArgs args, bool isOver) { var handledInManaged = false; @@ -774,15 +773,14 @@ private bool OnNativePointerMoveWithOverCheck(PointerRoutedEventArgs args, bool return handledInManaged; } - private bool OnNativePointerMove(PointerRoutedEventArgs args) => OnPointerMove(args, isManagedBubblingEvent: false); - private void OnManagePointerMove(PointerRoutedEventArgs args) => OnPointerMove(args, isManagedBubblingEvent: true); + private bool OnNativePointerMove(PointerRoutedEventArgs args) => OnPointerMove(args); - private bool OnPointerMove(PointerRoutedEventArgs args, bool isManagedBubblingEvent) + private bool OnPointerMove(PointerRoutedEventArgs args, BubblingContext ctx = default) { var handledInManaged = false; var isOverOrCaptured = ValidateAndUpdateCapture(args); - if (!isManagedBubblingEvent && isOverOrCaptured) + if (!ctx.IsLocalOnly && isOverOrCaptured) { // If this pointer was wrongly dispatched here (out of the bounds and not captured), // we don't raise the 'move' event @@ -795,7 +793,7 @@ private bool OnPointerMove(PointerRoutedEventArgs args, bool isManagedBubblingEv { // We need to process only events that were not handled by a child control, // so we should not use them for gesture recognition. - _gestures.Value.ProcessMoveEvents(args.GetIntermediatePoints(this), !isManagedBubblingEvent || isOverOrCaptured); + _gestures.Value.ProcessMoveEvents(args.GetIntermediatePoints(this), !ctx.IsLocalOnly || isOverOrCaptured); if (_gestures.Value.IsDragging) { global::Windows.UI.Xaml.Window.Current.DragDrop.ProcessMoved(args); @@ -805,15 +803,14 @@ private bool OnPointerMove(PointerRoutedEventArgs args, bool isManagedBubblingEv return handledInManaged; } - private bool OnNativePointerUp(PointerRoutedEventArgs args) => OnPointerUp(args, isManagedBubblingEvent: false); - private void OnManagedPointerUp(PointerRoutedEventArgs args) => OnPointerUp(args, isManagedBubblingEvent: true); + private bool OnNativePointerUp(PointerRoutedEventArgs args) => OnPointerUp(args); - private bool OnPointerUp(PointerRoutedEventArgs args, bool isManagedBubblingEvent) + private bool OnPointerUp(PointerRoutedEventArgs args, BubblingContext ctx = default) { var handledInManaged = false; var isOverOrCaptured = ValidateAndUpdateCapture(args, out var isOver); - handledInManaged |= SetPressed(args, false, muteEvent: isManagedBubblingEvent || !isOverOrCaptured); + handledInManaged |= SetPressed(args, false, muteEvent: ctx.IsLocalOnly || !isOverOrCaptured); // Note: We process the UpEvent between Release and Exited as the gestures like "Tap" @@ -824,7 +821,7 @@ private bool OnPointerUp(PointerRoutedEventArgs args, bool isManagedBubblingEven // if they are bubbling in managed it means that they where handled a child control, // so we should not use them for gesture recognition. var isDragging = _gestures.Value.IsDragging; - _gestures.Value.ProcessUpEvent(args.GetCurrentPoint(this), !isManagedBubblingEvent || isOverOrCaptured); + _gestures.Value.ProcessUpEvent(args.GetCurrentPoint(this), !ctx.IsLocalOnly || isOverOrCaptured); if (isDragging) { global::Windows.UI.Xaml.Window.Current.DragDrop.ProcessDropped(args); @@ -842,15 +839,14 @@ private bool OnPointerUp(PointerRoutedEventArgs args, bool isManagedBubblingEven return handledInManaged; } - private bool OnNativePointerExited(PointerRoutedEventArgs args) => OnPointerExited(args, isManagedBubblingEvent: false); - private void OnManagedPointerExited(PointerRoutedEventArgs args) => OnPointerExited(args, isManagedBubblingEvent: true); + private bool OnNativePointerExited(PointerRoutedEventArgs args) => OnPointerExited(args); - private bool OnPointerExited(PointerRoutedEventArgs args, bool isManagedBubblingEvent) + private bool OnPointerExited(PointerRoutedEventArgs args, BubblingContext ctx = default) { var handledInManaged = false; var isOverOrCaptured = ValidateAndUpdateCapture(args); - handledInManaged |= SetOver(args, false, muteEvent: isManagedBubblingEvent || !isOverOrCaptured); + handledInManaged |= SetOver(args, false, muteEvent: ctx.IsLocalOnly || !isOverOrCaptured); if (_gestures.IsValueCreated && _gestures.Value.IsDragging) { @@ -877,11 +873,10 @@ private bool OnPointerExited(PointerRoutedEventArgs args, bool isManagedBubbling private bool OnNativePointerCancel(PointerRoutedEventArgs args, bool isSwallowedBySystem) { args.CanceledByDirectManipulation = isSwallowedBySystem; - return OnPointerCancel(args, isManagedBubblingEvent: false); + return OnPointerCancel(args); } - private void OnManagedPointerCancel(PointerRoutedEventArgs args) => OnPointerCancel(args, isManagedBubblingEvent: true); - private bool OnPointerCancel(PointerRoutedEventArgs args, bool isManagedBubblingEvent) + private bool OnPointerCancel(PointerRoutedEventArgs args, BubblingContext ctx = default) { var isOverOrCaptured = ValidateAndUpdateCapture(args); // Check this *before* updating the pressed / over states! @@ -911,7 +906,7 @@ private bool OnPointerCancel(PointerRoutedEventArgs args, bool isManagedBubbling else { args.Handled = false; - handledInManaged |= !isManagedBubblingEvent && RaisePointerEvent(PointerCanceledEvent, args); + handledInManaged |= !ctx.IsLocalOnly && RaisePointerEvent(PointerCanceledEvent, args); handledInManaged |= SetNotCaptured(args); } @@ -922,15 +917,19 @@ private bool OnNativePointerWheel(PointerRoutedEventArgs args) { return RaisePointerEvent(PointerWheelChangedEvent, args); } + private bool OnPointerWheel(PointerRoutedEventArgs args, BubblingContext ctx = default) + { + return RaisePointerEvent(PointerWheelChangedEvent, args); + } private static (UIElement sender, RoutedEvent @event, PointerRoutedEventArgs args) _pendingRaisedEvent; - private bool RaisePointerEvent(RoutedEvent evt, PointerRoutedEventArgs args) + private bool RaisePointerEvent(RoutedEvent evt, PointerRoutedEventArgs args, BubblingContext ctx = default) { try { _pendingRaisedEvent = (this, evt, args); - return RaiseEvent(evt, args); + return RaiseEvent(evt, args, ctx); } catch (Exception e) { @@ -1214,24 +1213,24 @@ private bool Capture(Pointer pointer, PointerCaptureKind kind, PointerRoutedEven return PointerCapture.GetOrCreate(pointer).TryAddTarget(this, kind, relatedArgs); } - private void Release(PointerCaptureKind kinds, PointerRoutedEventArgs relatedARgs = null, bool muteEvent = false) + private void Release(PointerCaptureKind kinds, PointerRoutedEventArgs relatedArgs = null, bool muteEvent = false) { if (PointerCapture.Any(out var captures)) { foreach (var capture in captures) { - Release(capture, kinds, relatedARgs, muteEvent); + Release(capture, kinds, relatedArgs, muteEvent); } } } - private bool Release(Pointer pointer, PointerCaptureKind kinds, PointerRoutedEventArgs relatedARgs = null, bool muteEvent = false) + private bool Release(Pointer pointer, PointerCaptureKind kinds, PointerRoutedEventArgs relatedArgs = null, bool muteEvent = false) { return PointerCapture.TryGet(pointer, out var capture) - && Release(capture, kinds, relatedARgs, muteEvent); + && Release(capture, kinds, relatedArgs, muteEvent); } - private bool Release(PointerCapture capture, PointerCaptureKind kinds, PointerRoutedEventArgs relatedARgs = null, bool muteEvent = false) + private bool Release(PointerCapture capture, PointerCaptureKind kinds, PointerRoutedEventArgs relatedArgs = null, bool muteEvent = false) { if (!capture.RemoveTarget(this, kinds, out var lastDispatched).HasFlag(PointerCaptureKind.Explicit)) { @@ -1243,13 +1242,13 @@ private bool Release(PointerCapture capture, PointerCaptureKind kinds, PointerRo return false; } - relatedARgs = relatedARgs ?? lastDispatched; - if (relatedARgs == null) + relatedArgs ??= lastDispatched; + if (relatedArgs == null) { return false; // TODO: We should create a new instance of event args with dummy location } - relatedARgs.Handled = false; - return RaisePointerEvent(PointerCaptureLostEvent, relatedARgs); + relatedArgs.Handled = false; + return RaisePointerEvent(PointerCaptureLostEvent, relatedArgs); } #endregion diff --git a/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs b/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs index 8eeff580dc57..5a4be2f5dfd7 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.RoutedEvents.cs @@ -602,6 +602,7 @@ internal bool RaiseEvent(RoutedEvent routedEvent, RoutedEventArgs args, Bubbling // [3] Any local handlers? var isHandled = IsHandled(args); if (!ctx.Mode.HasFlag(BubblingMode.IgnoreElement) + && !ctx.IsInternal && _eventHandlerStore.TryGetValue(routedEvent, out var handlers) && handlers.Any()) { @@ -682,13 +683,8 @@ private static bool RaiseOnParent(RoutedEvent routedEvent, RoutedEventArgs args, { mode |= BubblingMode.IgnoreParents; } - ctx = new BubblingContext - { - Mode = mode, - Root = ctx.Root - }; - var handledByAnyParent = parent.RaiseEvent(routedEvent, args, ctx); + var handledByAnyParent = parent.RaiseEvent(routedEvent, args, ctx.WithMode(mode)); return handledByAnyParent; } @@ -737,6 +733,16 @@ internal struct BubblingContext { public static readonly BubblingContext Bubble = default; + public static readonly BubblingContext NoBubbling = new BubblingContext { Mode = BubblingMode.NoBubbling }; + + /// + /// When bubbling in managed code, the will take care to raise the event on each parent, + /// considering the Handled flag. + /// This value is used to flag events that are sent to element to maintain their internal state, + /// but which are not meant to initiate a new event bubbling (a.k.a. invoke the "RaiseEvent" again) + /// + public static readonly BubblingContext OnManagedBubbling = new BubblingContext{Mode = BubblingMode.NoBubbling, IsInternal = true}; + public static BubblingContext BubbleUpTo(UIElement root) => new BubblingContext {Root = root}; @@ -750,8 +756,38 @@ public static BubblingContext BubbleUpTo(UIElement root) /// /// It's expected that the event is raised on this Root element. public UIElement Root { get; set; } + + /// + /// Indicates that the associated event should not be publicly raised. + /// + /// + /// The "internal" here refers only to the private state of the code which has initiated this event, not subclasses. + /// This means that an event flagged as "internal" can bubble to update the private state of parents, + /// but the UIElement.RoutedEvent won't be raised in any way (public and internal handlers) and it won't be sent to Control.On() neither. + /// + public bool IsInternal { get; set; } + + /// + /// Indicates that the associated event is an internal event that will not be propagated to parent (cf. ). + /// + public bool IsLocalOnly => IsInternal && Mode == BubblingMode.NoBubbling; + + public BubblingContext WithMode(BubblingMode mode) => new BubblingContext + { + Mode = mode, + Root = Root, + IsInternal = IsInternal + }; } + /// + /// Defines the mode used to bubble an event. + /// + /// + /// This takes priority over the . + /// Preventing default bubble behavior of an event is meant to be used only when the event has already been raised/bubbled, + /// but we need to sent it also to some specific elements (e.g. implicit captures). + /// [Flags] internal enum BubblingMode { @@ -766,14 +802,14 @@ internal enum BubblingMode IgnoreElement = 1, /// - /// The event should be bubble to parent elements + /// The event should not bubble to parent elements /// IgnoreParents = 2, /// - /// The bubbling should stop here (the event won't be raised on the element) + /// The bubbling should stop here (the event won't even be raised on the current element) /// - StopBubbling = IgnoreElement | IgnoreParents, + NoBubbling = IgnoreElement | IgnoreParents, } private static bool IsHandled(RoutedEventArgs args)