Skip to content

Commit

Permalink
refactor: Cleanup hit test coercion
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Aug 16, 2024
1 parent 3651fcc commit de444e9
Show file tree
Hide file tree
Showing 29 changed files with 130 additions and 323 deletions.
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 @@ -321,6 +321,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 @@ -249,6 +249,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 @@ -197,8 +197,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 @@ -64,11 +64,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
30 changes: 16 additions & 14 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 void 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,23 @@ 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 +594,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 @@ -663,10 +665,10 @@ internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementA
// We didn't find any child at the given position, validate that element can be touched,
// and the position is in actual bounds(which might be different than the clipping bounds)
#if __SKIA__
if (renderingBounds.Contains(testPosition) && element.HitTest(transformToElement.Inverse().Transform(testPosition)))
if (!isForDrop && renderingBounds.Contains(testPosition) && element.HitTest(transformToElement.Inverse().Transform(testPosition)))
#else

if (elementHitTestVisibility == HitTestability.Visible && renderingBounds.Contains(testPosition)
if (!isForDrop && element.IsViewHitOrTagExternallyDefined() && renderingBounds.Contains(testPosition)
#if __WASM__
&& element.HitTest(testPosition)
#endif
Expand Down
Loading

0 comments on commit de444e9

Please sign in to comment.