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 Jun 29, 2024
1 parent 7dca68c commit ce42008
Show file tree
Hide file tree
Showing 24 changed files with 76 additions and 330 deletions.
3 changes: 1 addition & 2 deletions src/Uno.UI.FluentTheme.v2/themeresources_v2.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10256,8 +10256,7 @@
<ControlTemplate TargetType="Button">
<!-- Uno specific (LinearGradientBrush borders): Additional Grid as template root needed #6457 -->
<Grid>
<!-- Uno workaround: template-bind ContentTemplateSelector because it's not automatically propagated from the ContentControl #6452 -->
<ContentPresenter x:Name="ContentPresenter" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" BackgroundSizing="{TemplateBinding BackgroundSizing}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}" ContentTransitions="{TemplateBinding ContentTransitions}" CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" AutomationProperties.AccessibilityView="Raw" local:AnimatedIcon.State="Normal" xmlns:local="using:Microsoft.UI.Xaml.Controls">
<ContentPresenter x:Name="ContentPresenter" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" BackgroundSizing="{TemplateBinding BackgroundSizing}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTransitions="{TemplateBinding ContentTransitions}" CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" AutomationProperties.AccessibilityView="Raw" local:AnimatedIcon.State="Normal" xmlns:local="using:Microsoft.UI.Xaml.Controls">
<ContentPresenter.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" />
</ContentPresenter.BackgroundTransition>
Expand Down
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 @@ -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.

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
6 changes: 2 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 @@ -144,6 +140,8 @@ private void OnIsEnabledChanged(DependencyPropertyChangedEventArgs args)
UpdateDOMProperties();
}
#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: 0 additions & 2 deletions src/Uno.UI/UI/Xaml/Controls/Image/Image.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ private ExceptionRoutedEventArgs ImageFailedConverter(object sender, string e)

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

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

This file was deleted.

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
1 change: 0 additions & 1 deletion src/Uno.UI/UI/Xaml/Documents/Hyperlink.wasm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ private void UpdateNavigationProperties(Uri navigateUri, NavigationTarget target
("href", uri)
);
}
UpdateHitTest();
}

internal override bool IsViewHit()
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.IsViewHit() && renderingBounds.Contains(testPosition))
{
TRACE($"> LEAF! ({element.GetDebugName()} is the OriginalSource) | stale branch: {stale?.ToString() ?? "-- none --"}");
return (element, stale);
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

0 comments on commit ce42008

Please sign in to comment.