Skip to content

Commit

Permalink
Merge pull request #5942 from unoplatform/dev/dr/bubblingImplcitCaptures
Browse files Browse the repository at this point in the history
[Skia][macOS] Fix some pointers event not being dispatched properly
  • Loading branch information
dr1rrb authored May 6, 2021
2 parents 3767acc + f7f9cf0 commit 88b75d0
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 148 deletions.
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/UIElement.PointerCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
170 changes: 78 additions & 92 deletions src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -299,62 +254,93 @@ 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.
// 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);

// 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
Expand Down
Loading

0 comments on commit 88b75d0

Please sign in to comment.