Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Cleanup hit test coercion #17351

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private void HostOnMouseEvent(InputEventArgs args, TypedEventHandler<object, Poi

// Is the pointer inside an element in the flyout layer? if so, raise in managed
var xamlRoot = WpfManager.XamlRootMap.GetRootForHost((IWpfXamlRootHost)_host!);
var managedHitTestResult = Microsoft.UI.Xaml.Media.VisualTreeHelper.SearchDownForTopMostElementAt(eventArgs.CurrentPoint.Position, xamlRoot!.VisualTree.PopupRoot!, Microsoft.UI.Xaml.Media.VisualTreeHelper.DefaultGetTestability, null);
var managedHitTestResult = Microsoft.UI.Xaml.Media.VisualTreeHelper.SearchDownForTopMostElementAt(eventArgs.CurrentPoint.Position, xamlRoot!.VisualTree.PopupRoot!, isForDrop: false, isStale: null, forceCollapsed: null);
if (managedHitTestResult.element is { })
{
@event?.Invoke(this, eventArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,6 @@ public void When_HTMLElement_ExternalElement_Override()
Assert.AreEqual("p", SUT.HtmlTag);
}

[TestMethod]
[RunsOnUIThread]
public async Task When_HTMLElement_ExternalElement_Override_Then_IsHitTestable()
{
var SUT = new MyCustomComponent();

TestServices.WindowHelper.WindowContent = SUT;
await TestServices.WindowHelper.WaitForIdle();
await TestServices.WindowHelper.WaitFor(() => SUT.IsLoaded);
await TestServices.WindowHelper.WaitForIdle();

Assert.AreEqual(HitTestability.Visible, SUT.HitTestVisibility);
}

[TestMethod]
[RunsOnUIThread]
public void When_HTMLElement_ExternalElement_Override_Twice()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,9 @@ public async Task When_Nested_In_Native_View()
var expected = new Rect(25, 205, 80, 40);
RectAssert.AreEqual(expected, bounds);

GetHitTestability getHitTestability = null;
getHitTestability = element => (element as FrameworkElement)?.Background != null ? (element.GetHitTestVisibility(), getHitTestability) : (HitTestability.Invisible, getHitTestability);

foreach (var point in GetPointsInside(bounds, perimeterOffset: 5))
{
var hitTest = VisualTreeHelper.HitTest(point, WindowHelper.WindowContent.XamlRoot, getHitTestability);
var hitTest = VisualTreeHelper.HitTest(point, WindowHelper.WindowContent.XamlRoot);
Assert.AreEqual(sut, hitTest.element);
}

Expand Down
24 changes: 0 additions & 24 deletions src/Uno.UI/UI/Xaml/Controls/Border/Border.crossruntime.cs

This file was deleted.

4 changes: 4 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/Border/Border.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ protected override void OnBackgroundChanged(DependencyPropertyChangedEventArgs e
UpdateBorder();
#endif
OnBackgroundChangedPartial();

#if __WASM__
RefreshPointerEvents();
#endif
}

partial void OnBackgroundChangedPartial();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ partial void InitializePlatform()
});
}

private IDisposable _nativeElementDisposable;

partial void TryRegisterNativeElement(object oldValue, object newValue)
{
if (IsNativeHost && IsInLiveTree)
Expand Down Expand Up @@ -130,11 +128,6 @@ partial void AttachNativeElement()
_nativeHosts.Add(this);
EffectiveViewportChanged += OnEffectiveViewportChanged;
LayoutUpdated += OnLayoutUpdated;
var visiblityToken = RegisterPropertyChangedCallback(HitTestVisibilityProperty, OnHitTestVisiblityChanged);
_nativeElementDisposable = Disposable.Create(() =>
{
UnregisterPropertyChangedCallback(HitTestVisibilityProperty, visiblityToken);
});
}

partial void DetachNativeElement(object content)
Expand All @@ -147,7 +140,6 @@ partial void DetachNativeElement(object content)
EffectiveViewportChanged -= OnEffectiveViewportChanged;
LayoutUpdated -= OnLayoutUpdated;
_nativeElementHostingExtension.Value!.DetachNativeElement(content);
_nativeElementDisposable?.Dispose();
}

private Size MeasureNativeElement(Size childMeasuredSize, Size availableSize)
Expand All @@ -156,11 +148,6 @@ private Size MeasureNativeElement(Size childMeasuredSize, Size availableSize)
return _nativeElementHostingExtension.Value!.MeasureNativeElement(Content, childMeasuredSize, availableSize);
}

private void OnHitTestVisiblityChanged(DependencyObject sender, DependencyProperty dp)
{
_nativeElementHostingExtension.Value!.ChangeNativeElementVisibility(Content, HitTestVisibility != HitTestability.Collapsed);
}

internal static void UpdateNativeHostContentPresentersOpacities()
{
foreach (var contentPresenter in _nativeHosts)
Expand All @@ -179,6 +166,8 @@ internal static void UpdateNativeHostContentPresentersOpacities()

private void OnLayoutUpdated(object sender, object e)
{
_nativeElementHostingExtension.Value!.ChangeNativeElementVisibility(Content, this.GetHitTestVisibility() != HitTestability.Collapsed);

// Not quite sure why we need to queue the arrange call, but the native element either explodes or doesn't
// respect alignments correctly otherwise. This is particularly relevant for the initial load.
DispatcherQueue.TryEnqueue(ArrangeNativeElement);
Expand Down
8 changes: 4 additions & 4 deletions src/Uno.UI/UI/Xaml/Controls/Control/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,6 @@ public bool IsEnabled

private void OnIsEnabledChanged(DependencyPropertyChangedEventArgs args)
{
#if UNO_HAS_MANAGED_POINTERS || __WASM__
UpdateHitTest();
#endif

_isEnabledChangedEventArgs ??= new IsEnabledChangedEventArgs();
_isEnabledChangedEventArgs.SourceEvent = args;

Expand All @@ -143,7 +139,11 @@ private void OnIsEnabledChanged(DependencyPropertyChangedEventArgs args)
{
UpdateDOMProperties();
}

RefreshPointerEvents();
#endif

ClearPointersStateIfNeeded();
}
#endregion

Expand Down
3 changes: 1 addition & 2 deletions src/Uno.UI/UI/Xaml/Controls/Flyout/FlyoutPopupPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ private protected override void OnPointerPressedDismissed(PointerRoutedEventArgs
}

var point = args.GetCurrentPoint(null);
var hitTestIgnoringThis = VisualTreeHelper.DefaultGetTestability.Except(XamlRoot?.VisualTree.PopupRoot as UIElement ?? this);
var (elementHitUnderOverlay, _) = VisualTreeHelper.HitTest(point.Position, passThroughElement.XamlRoot, hitTestIgnoringThis);
var (elementHitUnderOverlay, _) = VisualTreeHelper.HitTest(point.Position, passThroughElement.XamlRoot, forceCollapsed: XamlRoot?.VisualTree.PopupRoot as UIElement ?? this);

if (elementHitUnderOverlay is null)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/Controls/Image/Image.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private ExceptionRoutedEventArgs ImageFailedConverter(object sender, string e)

partial void OnSourceChanged(ImageSource newValue, bool forceReload)
{
UpdateHitTest();
RefreshPointerEvents();

_lastMeasuredSize = _zeroSize;
// Hide the old image until the new image is loaded. This is the behaviour on WinUI.
Expand Down
10 changes: 0 additions & 10 deletions src/Uno.UI/UI/Xaml/Controls/Panel/Panel.crossruntime.cs

This file was deleted.

4 changes: 4 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/Panel/Panel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ protected override void OnBackgroundChanged(DependencyPropertyChangedEventArgs e
UpdateBorder();
#endif
OnBackgroundChangedPartial();

#if __WASM__
RefreshPointerEvents();
#endif
}

partial void OnBackgroundChangedPartial();
Expand Down
2 changes: 0 additions & 2 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,6 @@ partial void OnIsTextSelectionEnabledChangedPartial()
partial void OnTextChangedPartial()
{
_textChanged = true;

UpdateHitTest();
}

partial void ClearTextPartial() => SetHtmlContent("");
Expand Down
4 changes: 0 additions & 4 deletions src/Uno.UI/UI/Xaml/Documents/Hyperlink.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,7 @@ private void UpdateNavigationProperties(Uri navigateUri, NavigationTarget target
("href", uri)
);
}
UpdateHitTest();
}

internal override bool IsViewHit()
=> NavigateUri != null || base.IsViewHit();
}

public enum NavigationTarget
Expand Down
16 changes: 1 addition & 15 deletions src/Uno.UI/UI/Xaml/DragDrop/DropUITarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,6 @@ namespace Microsoft.UI.Xaml
{
internal class DropUITarget(XamlRoot xamlRoot) : ICoreDropOperationTarget
{
private static GetHitTestability? _getDropHitTestability;
private static GetHitTestability GetDropHitTestability => _getDropHitTestability ??= (elt =>
{
var visiblity = elt.GetHitTestVisibility();
return visiblity switch
{
HitTestability.Collapsed => (HitTestability.Collapsed, _getDropHitTestability!),
// Once we reached an element that AllowDrop, we only validate the hit testability for its children
_ when elt.AllowDrop => (visiblity, VisualTreeHelper.DefaultGetTestability),
_ => (HitTestability.Invisible, _getDropHitTestability!)
};
});


// Note: As drag events are routed (so they may be received by multiple elements), we might not have an entry for each drop targets.
// We will instead have entry only for leaf (a.k.a. OriginalSource).
// This is valid as UWP does clear the UIOverride as soon as a DragLeave is raised, no matter the number of drop target under pointer.
Expand Down Expand Up @@ -116,7 +102,7 @@ public IAsyncOperation<DataPackageOperation> DropAsync(CoreDragInfo dragInfo)
var target = VisualTreeHelper.HitTest(
dragInfo.Position,
xamlRoot,
getTestability: GetDropHitTestability,
isForDrop: true,
isStale: new StalePredicate(elt => elt.IsDragOver(dragInfo.SourceId), "IsDragOver"));

// First raise the drag leave event on stale branch if any.
Expand Down
2 changes: 0 additions & 2 deletions src/Uno.UI/UI/Xaml/FrameworkElement.unittests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ public partial class FrameworkElement : IEnumerable

internal bool ShouldInterceptInvalidate { get; set; }

internal void UpdateHitTest() { }

private protected virtual void OnPostLoading() { }

partial void OnLoadingPartial();
Expand Down
27 changes: 14 additions & 13 deletions src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,12 @@ internal static IReadOnlyList<_View> ClearChildren(UIElement view)
#endif
}

internal static readonly GetHitTestability DefaultGetTestability = elt => (elt.GetHitTestVisibility(), DefaultGetTestability!);

internal static (UIElement? element, Branch? stale) HitTest(
Point position,
XamlRoot? xamlRoot,
GetHitTestability? getTestability = null,
StalePredicate? isStale = null
bool isForDrop = false,
StalePredicate? isStale = null,
UIElement? forceCollapsed = null
#if TRACE_HIT_TESTING
, [CallerMemberName] string caller = "")
{
Expand All @@ -440,7 +439,7 @@ internal static (UIElement? element, Branch? stale) HitTest(
#endif
if (xamlRoot?.VisualTree.RootElement is UIElement root)
{
return SearchDownForTopMostElementAt(position, root, getTestability ?? DefaultGetTestability, isStale);
return SearchDownForTopMostElementAt(position, root, isForDrop, isStale, forceCollapsed);
}

return default;
Expand All @@ -453,20 +452,22 @@ internal static (UIElement? element, Branch? stale) HitTest(
internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementAt(
Point position,
UIElement element,
GetHitTestability getVisibility,
StalePredicate? isStale)
bool isForDrop,
StalePredicate? isStale,
UIElement? forceCollapsed)
{
var stale = default(Branch?);
HitTestability elementHitTestVisibility;
(elementHitTestVisibility, getVisibility) = getVisibility(element);

#if TRACE_HIT_TESTING
using var _ = SET_TRACE_SUBJECT(element);
TRACE($"- hit test visibility: {elementHitTestVisibility}");
#endif
if (isForDrop && element.AllowDrop)
{
isForDrop = false;
}

// If the element is not hit testable, do not even try to validate it nor its children.
if (elementHitTestVisibility == HitTestability.Collapsed)
if (element.Visibility == Visibility.Collapsed || !element.IsEnabledOverride() || !element.IsHitTestVisible || element == forceCollapsed)
{
// Even if collapsed, if the element is stale, we search down for the real stale leaf
if (isStale?.Method.Invoke(element) ?? false)
Expand Down Expand Up @@ -592,7 +593,7 @@ internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementA

while (child.MoveNext())
{
var childResult = SearchDownForTopMostElementAt(testPosition, child.Current!, getVisibility, isChildStale);
var childResult = SearchDownForTopMostElementAt(testPosition, child.Current!, isForDrop, isChildStale, forceCollapsed);

// If we found a stale element in child sub-tree, keep it and stop looking for stale elements
if (childResult.stale is not null)
Expand Down Expand Up @@ -662,7 +663,7 @@ internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementA

// We didn't find any child at the given position, validate that element can be touched (i.e. not HitTestability.Invisible),
// and the position is in actual bounds (which might be different than the clipping bounds)
if (elementHitTestVisibility == HitTestability.Visible && renderingBounds.Contains(testPosition))
if (!isForDrop && element.IsViewHitOrTagExternallyDefined() && renderingBounds.Contains(testPosition))
{
TRACE($"> LEAF! ({element.GetDebugName()} is the OriginalSource) | stale branch: {stale?.ToString() ?? "-- none --"}");
return (element, stale);
Expand Down
6 changes: 6 additions & 0 deletions src/Uno.UI/UI/Xaml/Shapes/Shape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,13 @@ public DoubleCollection StrokeDashArray
#endregion

internal override bool IsViewHit()
#if __WASM__
// We stay at the default "pointer-events: none" defined in Uno.UI.css in class svg.uno-uielement.
// This is required to avoid this SVG element (which is actually only a collection) to stoll pointer events.
=> false;
#else
=> Fill != null; // Do not invoke base.IsViewHit(): We don't have to have de FrameworkElement.Background to be hit testable!
#endif

protected override void OnBackgroundChanged(DependencyPropertyChangedEventArgs e)
{
Expand Down
7 changes: 0 additions & 7 deletions src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ private protected void UpdateRender()
UpdateStrokeDashArray();
}


private protected override void OnHitTestVisibilityChanged(HitTestability oldValue, HitTestability newValue)
{
// We don't invoke the base, so we stay at the default "pointer-events: none" defined in Uno.UI.css in class svg.uno-uielement.
// This is required to avoid this SVG element (which is actually only a collection) to stoll pointer events.
}

private void OnFillBrushChanged()
{
// We don't request an update of the HitTest (UpdateHitTest()) since this element is never expected to be hit testable.
Expand Down
Loading
Loading