Skip to content

Commit

Permalink
feat: Add support for the Flyou.OverlayInputPassThroughElement on all…
Browse files Browse the repository at this point in the history
… platforms
  • Loading branch information
dr1rrb committed Apr 19, 2024
1 parent dfba9df commit 7357b94
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 50 deletions.
38 changes: 21 additions & 17 deletions src/Uno.UI/UI/Xaml/Controls/Flyout/FlyoutPopupPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Uno.UI.Extensions;
using Uno.UI.Xaml.Core;

namespace Microsoft.UI.Xaml.Controls
{
Expand Down Expand Up @@ -39,28 +41,30 @@ public FlyoutBasePopupPanel(FlyoutBase flyout) : base(flyout._popup)

protected override int PopupPlacementTargetMargin => 5;

private protected override void OnPointerPressed(object sender, PointerRoutedEventArgs args)
private protected override void OnPointerPressedDismissed(PointerRoutedEventArgs args)
{
base.OnPointerPressed(sender, args);

// Make sure we are the original source. We do not want to handle PointerPressed on the Popup itself.
if (args.OriginalSource == this)
if (Flyout.OverlayInputPassThroughElement is not UIElement passThroughElement
|| !Flyout.EnumerateAncestors().Contains(passThroughElement))
{
if (Flyout.OverlayInputPassThroughElement is UIElement passThroughElement)
{
var (elementToBeHit, _) = VisualTreeHelper.SearchDownForTopMostElementAt(
args.GetCurrentPoint(null).Position,
passThroughElement.XamlRoot.VisualTree.RootElement,
VisualTreeHelper.DefaultGetTestability,
childrenFilter: elements => elements.Where(e => e != this));
// The element must be a parent of the Flyout (not 'this') to be able to receive the pointer events.
return;
}

var eventArgs = new PointerRoutedEventArgs(
new PointerEventArgs(args.GetCurrentPoint(null), args.KeyModifiers),
args.OriginalSource as UIElement);
var point = args.GetCurrentPoint(null);
var hitTestIgnoringThis = VisualTreeHelper.DefaultGetTestability.Except(this);
var (elementHitUnderOverlay, _) = VisualTreeHelper.HitTest(point.Position, passThroughElement.XamlRoot, hitTestIgnoringThis);

elementToBeHit.OnPointerDown(eventArgs);
}
if (elementHitUnderOverlay?.EnumerateAncestors().Contains(passThroughElement) is not true)
{
// The element found by the HitTest is not a child of the pass-through element.
return;
}

#if UNO_HAS_MANAGED_POINTERS
XamlRoot?.VisualTree.ContentRoot.InputManager.Pointers.ReRoute(args, from: this, to: elementHitUnderOverlay);
#else
XamlRoot?.VisualTree.RootVisual.ReRoutePointerDownEvent(args, from: this, to: elementHitUnderOverlay);
#endif
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/Uno.UI/UI/Xaml/Controls/Popup/PopupPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ private protected override void OnUnloaded()

// TODO: pointer handling should really go on PopupRoot. For now it's easier to put here because PopupRoot doesn't track open popups, and also we
// need to support native popups on Android that don't use PopupRoot.
private protected virtual void OnPointerPressed(object sender, PointerRoutedEventArgs args)
private void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{
// Make sure we are the original source. We do not want to handle PointerPressed on the Popup itself.
if (args.OriginalSource == this && Popup is { } popup)
Expand All @@ -260,12 +260,15 @@ private protected virtual void OnPointerPressed(object sender, PointerRoutedEven
// disabled for ContentDialogs.
else if (popup.IsLightDismissEnabled)
{
OnPointerPressedDismissed(args);
ClosePopup(popup);
}
args.Handled = true;
}
}

private protected virtual void OnPointerPressedDismissed(PointerRoutedEventArgs args) { }

private static void ClosePopup(Popup popup)
{
// Give the popup an opportunity to cancel closing.
Expand Down
34 changes: 34 additions & 0 deletions src/Uno.UI/UI/Xaml/HitTestability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,38 @@ internal enum HitTestability
internal record struct StalePredicate(PredicateOfUIElement Method, string Name);

internal delegate bool PredicateOfUIElement(UIElement element);

internal static class GetHitTestabilityExtensions
{
/// <summary>
/// Wrap the given hitTest delegate to exclude (i.e. consider as <see cref="HitTestability.Invisible"/>) the provided element.
/// </summary>
/// <param name="hitTest">The hit-testing delegate to wrap.</param>
/// <param name="element">The element that should be considered as <see cref="HitTestability.Invisible"/>)</param>
/// <returns></returns>
internal static GetHitTestability Except(this GetHitTestability hitTest, UIElement element)
{
GetHitTestability hitTestExceptElement = default!;
hitTestExceptElement = elt =>
{
if (elt == element)
{
return (HitTestability.Invisible, hitTest);
}
else
{
var (hitTestability, childrenGetHitTestability) = hitTest(elt);

// If the childrenGetHitTestability is no longer the provided 'hitTest' we need to re-wrap it!
childrenGetHitTestability = childrenGetHitTestability == hitTest
? hitTestExceptElement!
: childrenGetHitTestability.Except(element);

return (hitTestability, childrenGetHitTestability);
}
};

return hitTestExceptElement;
}
}
}
2 changes: 2 additions & 0 deletions src/Uno.UI/UI/Xaml/Input/PointerRoutedEventArgs.Managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ partial class PointerRoutedEventArgs
private readonly PointerEventArgs _pointerEventArgs;
private readonly PointerPoint _currentPoint;

internal Windows.UI.Core.PointerEventArgs CoreArgs => _pointerEventArgs;

internal PointerRoutedEventArgs(
PointerEventArgs pointerEventArgs,
UIElement source) : this()
Expand Down
146 changes: 117 additions & 29 deletions src/Uno.UI/UI/Xaml/Internal/InputManager.Pointers.Managed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Uno.Foundation.Extensibility;
using Uno.Foundation.Logging;
using Uno.UI.Extensions;
Expand All @@ -16,6 +18,8 @@
using Windows.UI.Input.Preview.Injection;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using static Microsoft.UI.Xaml.UIElement;
Expand Down Expand Up @@ -103,16 +107,91 @@ public void Init(object host)
_source.PointerCancelled += (c, e) => OnPointerCancelled(e);
}

private void UpdateLastInputType(PointerEventArgs e)
#region Current event dispatching transaction
private PointerDispatching? _current;

/// <summary>
/// Gets the currently dispatched event.
/// </summary>
/// <remarks>This is set only while a pointer event is currently being dispatched.</remarks>
internal PointerRoutedEventArgs? Current => _current?.Args;

private PointerDispatching StartDispatch(in PointerEvent evt, in PointerRoutedEventArgs args)
=> new(this, evt, args);

private readonly record struct PointerDispatching : IDisposable
{
_inputManager.LastInputDeviceType = e.CurrentPoint?.PointerDeviceType switch
private readonly PointerManager _manager;
public PointerEvent Event { get; }
public PointerRoutedEventArgs Args { get; }

public PointerDispatching(PointerManager manager, PointerEvent @event, PointerRoutedEventArgs args)
{
PointerDeviceType.Touch => InputDeviceType.Touch,
PointerDeviceType.Pen => InputDeviceType.Pen,
PointerDeviceType.Mouse => InputDeviceType.Mouse,
_ => _inputManager.LastInputDeviceType
};
_manager = manager;
Args = args;
Event = @event;

// Before any dispatch, we make sure to reset the event to it's original state
Debug.Assert(args.CanBubbleNatively == PointerRoutedEventArgs.PlatformSupportsNativeBubbling);
args.Reset();

// Set us as the current dispatching
if (_manager._current is not null)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"A pointer is already being processed {_manager._current} while trying to raise {this}");
}
Debug.Fail($"A pointer is already being processed {_manager._current} while trying to raise {this}.");
}
_manager._current = this;

// Then notify all external components that the dispatching is starting
_manager._inputManager.LastInputDeviceType = args.Pointer.PointerDeviceType switch
{
PointerDeviceType.Touch => InputDeviceType.Touch,
PointerDeviceType.Pen => InputDeviceType.Pen,
PointerDeviceType.Mouse => InputDeviceType.Mouse,
_ => _manager._inputManager.LastInputDeviceType
};
UIElement.BeginPointerEventDispatch();
}

public PointerEventDispatchResult End()
{
Dispose();
var result = UIElement.EndPointerEventDispatch();

// Once this dispatching has been removed from the _current dispatch (i.e. dispatch is effectively completed),
// we re-dispatch the event to the requested target (if any)
// Note: We create a new PointerRoutedEventArgs with a new OriginalSource == reRouted.To
if (_manager._reRouted is { } reRouted)
{
// Note: Here we are not validating the current result.VisualTreeAltered nor we perform a new hit test as we should if `true`
// This is valid only because the single element that is able to re-route the event is the PopupRoot, which is already at the top of the visual tree.
// When the PopupRoot perform teh HitTest, the visual tree is already updated.
result += _manager.Raise(
Event,
new VisualTreeHelper.Branch(reRouted.From, reRouted.To),
new PointerRoutedEventArgs(reRouted.Args.CoreArgs, reRouted.To) { CanBubbleNatively = false });
}

return result;
}

/// <inheritdoc />
public override string ToString()
=> $"[{Event.Name}] {Args.Pointer.UniqueId}";

public void Dispose()
{
if (_manager._current == this)
{
_manager._current = null;
}
}
}
#endregion

private void OnPointerWheelChanged(Windows.UI.Core.PointerEventArgs args)
{
Expand Down Expand Up @@ -223,8 +302,6 @@ private void OnPointerEntered(Windows.UI.Core.PointerEventArgs args)
Trace($"PointerEntered [{originalSource.GetDebugName()}]");
}

UpdateLastInputType(args);

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

Raise(Enter, originalSource, routedArgs);
Expand Down Expand Up @@ -265,8 +342,6 @@ private void OnPointerExited(Windows.UI.Core.PointerEventArgs args)
Trace($"PointerExited [{overBranchLeaf.GetDebugName()}]");
}

UpdateLastInputType(args);

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

Raise(Leave, overBranchLeaf, routedArgs);
Expand Down Expand Up @@ -307,8 +382,6 @@ private void OnPointerPressed(Windows.UI.Core.PointerEventArgs args)
Trace($"PointerPressed [{originalSource.GetDebugName()}]");
}

UpdateLastInputType(args);

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

_pressedElements[routedArgs.Pointer] = originalSource;
Expand Down Expand Up @@ -346,8 +419,6 @@ private void OnPointerReleased(Windows.UI.Core.PointerEventArgs args)
Trace($"PointerReleased [{originalSource.GetDebugName()}]");
}

UpdateLastInputType(args);

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

RaiseUsingCaptures(Released, originalSource, routedArgs, false);
Expand Down Expand Up @@ -392,8 +463,6 @@ private void OnPointerMoved(Windows.UI.Core.PointerEventArgs args)
Trace($"PointerMoved [{originalSource.GetDebugName()}]");
}

UpdateLastInputType(args);

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

// First raise the PointerExited events on the stale branch
Expand Down Expand Up @@ -446,8 +515,6 @@ private void OnPointerCancelled(Windows.UI.Core.PointerEventArgs args)
Trace($"PointerCancelled [{originalSource.GetDebugName()}]");
}

UpdateLastInputType(args);

var routedArgs = new PointerRoutedEventArgs(args, originalSource);

RaiseUsingCaptures(Cancelled, originalSource, routedArgs, false);
Expand All @@ -456,6 +523,28 @@ private void OnPointerCancelled(Windows.UI.Core.PointerEventArgs args)
ClearPressedState(routedArgs);
}

/// <summary>
/// Re-route the given event args (cf. <see cref="FlyoutBase.OverlayInputPassThroughElement"/>).
/// </summary>
public void ReRoute(PointerRoutedEventArgs routedArgs, UIElement from, UIElement to)
{
if (Current != routedArgs)
{
throw new InvalidOperationException("Cannot reroute a pointer event args that is not currently being dispatched.");
}

if (_reRouted is not null)
{
throw new InvalidOperationException("Pointer event args can be re-routed only once per bubbling.");
}

_reRouted = new ReRouted(routedArgs, from, to);
}

private ReRouted? _reRouted;

private readonly record struct ReRouted(PointerRoutedEventArgs Args, UIElement From, UIElement To);

#region Captures
internal void SetPointerCapture(PointerIdentifier uniqueId)
{
Expand Down Expand Up @@ -559,32 +648,33 @@ private PointerEventDispatchResult Raise(PointerEvent evt, UIElement originalSou
}

routedArgs.Handled = false;
UIElement.BeginPointerEventDispatch();
using var dispatch = StartDispatch(evt, routedArgs);

evt.Invoke(originalSource, routedArgs, BubblingContext.Bubble);

return EndPointerEventDispatch();
return dispatch.End();
}

private PointerEventDispatchResult Raise(PointerEvent evt, VisualTreeHelper.Branch branch, PointerRoutedEventArgs routedArgs)
{
using var _ = StartDispatch(evt, routedArgs);
if (_trace)
{
Trace($"[Ignoring captures] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to branch [{branch}]");
}

routedArgs.Handled = false;
UIElement.BeginPointerEventDispatch();
using var dispatch = StartDispatch(evt, routedArgs);

evt.Invoke(branch.Leaf, routedArgs, BubblingContext.BubbleUpTo(branch.Root));

return UIElement.EndPointerEventDispatch();
return dispatch.End();
}

private PointerEventDispatchResult RaiseUsingCaptures(PointerEvent evt, UIElement originalSource, PointerRoutedEventArgs routedArgs, bool setCursor)
{
routedArgs.Handled = false;
UIElement.BeginPointerEventDispatch();
using var dispatch = StartDispatch(evt, routedArgs);

if (PointerCapture.TryGet(routedArgs.Pointer, out var capture))
{
Expand All @@ -605,8 +695,7 @@ private PointerEventDispatchResult RaiseUsingCaptures(PointerEvent evt, UIElemen
Trace($"[Implicit capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to capture target [{originalSource.GetDebugName()}] (-- no bubbling--)");
}

routedArgs.Handled = false;
evt.Invoke(target.Element, routedArgs, BubblingContext.NoBubbling);
evt.Invoke(target.Element, routedArgs.Reset(), BubblingContext.NoBubbling);
}

if (setCursor)
Expand Down Expand Up @@ -637,8 +726,7 @@ private PointerEventDispatchResult RaiseUsingCaptures(PointerEvent evt, UIElemen
Trace($"[Explicit capture] raising event {evt.Name} (args: {routedArgs.GetHashCode():X8}) to alternative (implicit) target [{explicitTarget.Element.GetDebugName()}] (-- no bubbling--)");
}

routedArgs.Handled = false;
evt.Invoke(target.Element, routedArgs, BubblingContext.NoBubbling);
evt.Invoke(target.Element, routedArgs.Reset(), BubblingContext.NoBubbling);
}

if (setCursor)
Expand All @@ -662,7 +750,7 @@ private PointerEventDispatchResult RaiseUsingCaptures(PointerEvent evt, UIElemen
}
}

return UIElement.EndPointerEventDispatch();
return dispatch.End();
}

private void SetSourceCursor(UIElement element)
Expand Down
Loading

0 comments on commit 7357b94

Please sign in to comment.