diff --git a/src/Uno.UI.FluentTheme.v2/themeresources_v2.xaml b/src/Uno.UI.FluentTheme.v2/themeresources_v2.xaml index 0b9a44d0e864..9567862bd575 100644 --- a/src/Uno.UI.FluentTheme.v2/themeresources_v2.xaml +++ b/src/Uno.UI.FluentTheme.v2/themeresources_v2.xaml @@ -10256,8 +10256,7 @@ - - + diff --git a/src/Uno.UI.Runtime.Skia.Wpf/Input/WpfCorePointerInputSource.cs b/src/Uno.UI.Runtime.Skia.Wpf/Input/WpfCorePointerInputSource.cs index 42255348336f..8d4719dd5ab2 100644 --- a/src/Uno.UI.Runtime.Skia.Wpf/Input/WpfCorePointerInputSource.cs +++ b/src/Uno.UI.Runtime.Skia.Wpf/Input/WpfCorePointerInputSource.cs @@ -141,7 +141,7 @@ private void HostOnMouseEvent(InputEventArgs args, TypedEventHandler SUT.IsLoaded); - await TestServices.WindowHelper.WaitForIdle(); - - Assert.AreEqual(HitTestability.Visible, SUT.HitTestVisibility); - } - [TestMethod] [RunsOnUIThread] public void When_HTMLElement_ExternalElement_Override_Twice() diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_VisualTreeHelper.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_VisualTreeHelper.cs index f64b8d74dfab..e18bfc86f37c 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_VisualTreeHelper.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Media/Given_VisualTreeHelper.cs @@ -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); } diff --git a/src/Uno.UI/UI/Xaml/Controls/Border/Border.crossruntime.cs b/src/Uno.UI/UI/Xaml/Controls/Border/Border.crossruntime.cs deleted file mode 100644 index 9a8fc08e3436..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Border/Border.crossruntime.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Uno.Extensions; -using System.Linq; -using System.Drawing; -using Uno.Disposables; -using Microsoft.UI.Xaml.Media; -using Uno.UI; - -using View = Microsoft.UI.Xaml.UIElement; -using Color = System.Drawing.Color; -using Microsoft.UI.Composition; -using System.Numerics; -using Windows.Foundation; -using Microsoft.UI.Xaml.Shapes; -using Uno.UI.Xaml.Controls; - -namespace Microsoft.UI.Xaml.Controls; - -partial class Border -{ - partial void OnBackgroundChangedPartial() => UpdateHitTest(); -} diff --git a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.skia.cs b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.skia.cs index b78a35afee12..b29451764b01 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.skia.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ContentPresenter/ContentPresenter.skia.cs @@ -42,8 +42,6 @@ partial void InitializePlatform() }); } - private IDisposable _nativeElementDisposable; - partial void TryRegisterNativeElement(object oldValue, object newValue) { if (IsNativeHost && IsInLiveTree) @@ -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) @@ -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) @@ -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) @@ -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); diff --git a/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs b/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs index 3de76a4a9060..0626eaccb11c 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Control/Control.cs @@ -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; @@ -144,6 +140,8 @@ private void OnIsEnabledChanged(DependencyPropertyChangedEventArgs args) UpdateDOMProperties(); } #endif + + ClearPointersStateIfNeeded(); } #endregion diff --git a/src/Uno.UI/UI/Xaml/Controls/Flyout/FlyoutPopupPanel.cs b/src/Uno.UI/UI/Xaml/Controls/Flyout/FlyoutPopupPanel.cs index fb96b3b5481f..9a6202a92558 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Flyout/FlyoutPopupPanel.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Flyout/FlyoutPopupPanel.cs @@ -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) { diff --git a/src/Uno.UI/UI/Xaml/Controls/Image/Image.wasm.cs b/src/Uno.UI/UI/Xaml/Controls/Image/Image.wasm.cs index 0cf0b59aa758..afa0288af177 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Image/Image.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Image/Image.wasm.cs @@ -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 diff --git a/src/Uno.UI/UI/Xaml/Controls/Panel/Panel.crossruntime.cs b/src/Uno.UI/UI/Xaml/Controls/Panel/Panel.crossruntime.cs deleted file mode 100644 index fee5fbcc0668..000000000000 --- a/src/Uno.UI/UI/Xaml/Controls/Panel/Panel.crossruntime.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections; -using Microsoft.UI.Xaml.Media; -using Uno.UI.Xaml.Controls; - -namespace Microsoft.UI.Xaml.Controls; - -partial class Panel -{ - partial void OnBackgroundChangedPartial() => UpdateHitTest(); -} diff --git a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs index 45a19607e13c..59d0440f2422 100644 --- a/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.wasm.cs @@ -192,8 +192,6 @@ partial void OnIsTextSelectionEnabledChangedPartial() partial void OnTextChangedPartial() { _textChanged = true; - - UpdateHitTest(); } partial void ClearTextPartial() => SetHtmlContent(""); diff --git a/src/Uno.UI/UI/Xaml/Documents/Hyperlink.wasm.cs b/src/Uno.UI/UI/Xaml/Documents/Hyperlink.wasm.cs index 7cff3680b208..7eb79caa54fd 100644 --- a/src/Uno.UI/UI/Xaml/Documents/Hyperlink.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Documents/Hyperlink.wasm.cs @@ -67,7 +67,6 @@ private void UpdateNavigationProperties(Uri navigateUri, NavigationTarget target ("href", uri) ); } - UpdateHitTest(); } internal override bool IsViewHit() diff --git a/src/Uno.UI/UI/Xaml/DragDrop/DropUITarget.cs b/src/Uno.UI/UI/Xaml/DragDrop/DropUITarget.cs index 36e1ec71c095..fb8bbb31df4e 100644 --- a/src/Uno.UI/UI/Xaml/DragDrop/DropUITarget.cs +++ b/src/Uno.UI/UI/Xaml/DragDrop/DropUITarget.cs @@ -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. @@ -116,7 +102,7 @@ public IAsyncOperation 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. diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.unittests.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.unittests.cs index b50900e797df..fe6df51005a9 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.unittests.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.unittests.cs @@ -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(); diff --git a/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs b/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs index 9dda3260e602..ec08e0e5ca86 100644 --- a/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs +++ b/src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs @@ -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 = "") { @@ -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; @@ -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) @@ -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) @@ -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); diff --git a/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs b/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs index f46caa449680..f1ff176cb2da 100644 --- a/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs @@ -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. diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs deleted file mode 100644 index 7475c668a2ca..000000000000 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.Managed.cs +++ /dev/null @@ -1,101 +0,0 @@ -#if UNO_HAS_MANAGED_POINTERS -#nullable enable - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Text; - -using Uno.Disposables; -using Uno.Extensions; -using Uno.Foundation.Logging; -using Uno.UI.DataBinding; -using Uno.UI.Extensions; -using Windows.UI.Core; -using Windows.Foundation; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using Uno.UI; -using Uno.UI.Xaml; -using Uno.UI.Xaml.Core; -using Uno.UI.Xaml.Islands; - -#if HAS_UNO_WINUI -using Microsoft.UI.Input; -#else -using Windows.Devices.Input; -using Windows.UI.Input; -#endif - -namespace Microsoft.UI.Xaml -{ - partial class UIElement - { - internal void UpdateHitTest() - { - this.CoerceValue(HitTestVisibilityProperty); - } - - /// - /// Represents the final calculated hit-test visibility of the element. - /// - /// - /// This property should never be directly set, and its value should always be calculated through coercion (see . - /// - [GeneratedDependencyProperty(DefaultValue = HitTestability.Collapsed, CoerceCallback = true, Options = FrameworkPropertyMetadataOptions.Inherits)] - internal static DependencyProperty HitTestVisibilityProperty { get; } = CreateHitTestVisibilityProperty(); - - internal HitTestability HitTestVisibility - { - get => GetHitTestVisibilityValue(); - set => SetHitTestVisibilityValue(value); - } - - /// - /// This calculates the final hit-test visibility of an element. - /// - /// - private object CoerceHitTestVisibility(object baseValue) - { - if (this is RootVisual or XamlIsland) - { - return HitTestability.Visible; - } - - // The HitTestVisibilityProperty is never set directly. This means that baseValue is always the result of the parent's CoerceHitTestVisibility. - var parentValue = baseValue == DependencyProperty.UnsetValue - ? HitTestability.Collapsed - : (HitTestability)baseValue; - - // If the parent is collapsed, we should be collapsed as well. This takes priority over everything else, even if we would be visible otherwise. - if (parentValue == HitTestability.Collapsed) - { - return HitTestability.Collapsed; - } - - // If we're not locally hit-test visible, visible, or enabled, we should be collapsed. Our children will be collapsed as well. - if ( -#if !__MACOS__ - !IsLoaded || -#endif - !IsHitTestVisible || Visibility != Visibility.Visible || !IsEnabledOverride()) - { - return HitTestability.Collapsed; - } - - // If we're not hit (usually means we don't have a Background/Fill), we're invisible. Our children will be visible or not, depending on their state. - if (!IsViewHit()) - { - return HitTestability.Invisible; - } - - // If we're not collapsed or invisible, we can be targeted by hit-testing. This means that we can be the source of pointer events. - return HitTestability.Visible; - } - } -} -#endif diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs index 60cdadd57323..88357c5f9a56 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs @@ -13,6 +13,7 @@ using Windows.Foundation; using Windows.UI.Core; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Uno.Extensions; @@ -22,6 +23,7 @@ using Uno.UI.Xaml; using Uno.UI.Xaml.Core; + #if HAS_UNO_WINUI using Microsoft.UI.Input; #else @@ -65,16 +67,6 @@ namespace Microsoft.UI.Xaml partial class UIElement { - static UIElement() - { - var uiElement = typeof(UIElement); - VisibilityProperty.GetMetadata(uiElement).MergePropertyChangedCallback(ClearPointersStateIfNeeded); - Microsoft.UI.Xaml.Controls.Control.IsEnabledProperty.GetMetadata(typeof(Microsoft.UI.Xaml.Controls.Control)).MergePropertyChangedCallback(ClearPointersStateIfNeeded); -#if UNO_HAS_ENHANCED_HIT_TEST_PROPERTY - HitTestVisibilityProperty.GetMetadata(uiElement).MergePropertyChangedCallback(ClearPointersStateIfNeeded); -#endif - } - #region ManipulationMode (DP) public static DependencyProperty ManipulationModeProperty { get; } = DependencyProperty.Register( "ManipulationMode", @@ -178,15 +170,13 @@ private void InitializePointers() private bool IsGestureRecognizerCreated => _gestures != null; - private static readonly PropertyChangedCallback ClearPointersStateIfNeeded = (DependencyObject sender, DependencyPropertyChangedEventArgs dp) => + private void ClearPointersStateOnVisibilityChangeIfNeeded(Visibility newVisibility) { // As the Visibility DP is not inherited, when a control becomes invisible we validate that if any // of its children is capturing a pointer, and we release those captures. // As the number of capture is usually really small, we assume that its more performant to walk the tree // when the visibility changes instead of creating another coalesced DP. - if (dp.NewValue is Visibility visibility - && visibility != Visibility.Visible - && PointerCapture.Any(out var captures)) + if (newVisibility != Visibility.Visible && PointerCapture.Any(out var captures)) { for (var captureIndex = 0; captureIndex < captures.Count; captureIndex++) { @@ -196,7 +186,7 @@ private void InitializePointers() for (var targetIndex = 0; targetIndex < targets.Count; targetIndex++) { var target = targets[targetIndex]; - if (target.Element.HasParent(sender)) + if (target.Element.HasParent(this)) { target.Element.Release(capture, PointerCaptureKind.Any); } @@ -204,12 +194,26 @@ private void InitializePointers() } } - if (sender is UIElement elt && elt.GetHitTestVisibility() == HitTestability.Collapsed) + ClearPointersStateIfNeeded(); + } + + private protected void ClearPointersStateIfNeeded() + { + if (Visibility == Visibility.Collapsed || !IsEnabledOverride() || !IsHitTestVisible) { _currentPointerEventDispatch.VisualTreeAltered = true; - elt.ClearPointerState(); + ClearPointerStateOnTree(); } - }; + } + + private void ClearPointerStateOnTree() + { + ClearPointerState(); + foreach (var child in VisualTreeHelper.GetManagedVisualChildren(this)) + { + child.ClearPointerStateOnTree(); + } + } private static readonly RoutedEventHandler ClearPointersStateOnUnload = (object sender, RoutedEventArgs args) => { @@ -267,7 +271,34 @@ internal struct PointerEventDispatchResult internal HitTestability GetHitTestVisibility() { #if __WASM__ || __SKIA__ || __MACOS__ - return HitTestVisibility; + var element = this; + while (element is not null) + { + if (element.Visibility == Visibility.Collapsed || !element.IsEnabledOverride() || !element.IsHitTestVisible) + { + return HitTestability.Collapsed; + } + + element = GetParentUIElement(element); + } + + return HitTestability.Visible; + + static UIElement GetParentUIElement(UIElement element) + { + var parent = VisualTreeHelper.GetParent(element); + while (parent is not null) + { + if (parent is UIElement parentUIElement) + { + return parentUIElement; + } + + parent = VisualTreeHelper.GetParent(parent); + } + + return null; + } #else // This is a coalesced HitTestVisible and should be unified with it // We should follow the WASM way and unify it on all platforms! @@ -276,7 +307,6 @@ internal HitTestability GetHitTestVisibility() { return HitTestability.Collapsed; } - if (this is Microsoft.UI.Xaml.Controls.Control ctrl) { return ctrl.IsLoaded && ctrl.IsEnabled diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.wasm.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.wasm.cs index 1b56726e2ded..a956767d5a22 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.wasm.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.wasm.cs @@ -36,8 +36,8 @@ namespace Microsoft.UI.Xaml; public partial class UIElement : DependencyObject { - private static TSInteropMarshaller.HandleRef _pointerEventArgs; - private static TSInteropMarshaller.HandleRef _pointerEventResult; + private static TSInteropMarshaller.HandleRef? _pointerEventArgs; + private static TSInteropMarshaller.HandleRef? _pointerEventResult; private static Microsoft/* UWP don't rename */.UI.Input.InputSystemCursorShape? _lastSetCursor; @@ -148,7 +148,7 @@ internal static void OnNativePointerEvent() { _logTrace?.Trace("Receiving native pointer event."); - var args = _pointerEventArgs.Value; + var args = _pointerEventArgs!.Value; var element = GetElementFromHandle(args.HtmlId); if (element is null) @@ -291,7 +291,7 @@ internal static void OnNativePointerEvent() } finally { - _pointerEventResult.Value = new NativePointerEventResult + _pointerEventResult!.Value = new NativePointerEventResult { Result = (byte)result.Value }; @@ -380,88 +380,6 @@ partial void OnManipulationModeChanged(ManipulationModes oldMode, ManipulationMo } } - #region HitTestVisibility - internal void UpdateHitTest() - { - this.CoerceValue(HitTestVisibilityProperty); - } - - [GeneratedDependencyProperty(DefaultValue = HitTestability.Collapsed, ChangedCallback = true, CoerceCallback = true, Options = FrameworkPropertyMetadataOptions.Inherits)] - internal static DependencyProperty HitTestVisibilityProperty { get; } = CreateHitTestVisibilityProperty(); - - internal HitTestability HitTestVisibility - { - get => GetHitTestVisibilityValue(); - set => SetHitTestVisibilityValue(value); - } - - /// - /// This calculates the final hit-test visibility of an element. - /// - /// - private object CoerceHitTestVisibility(object baseValue) - { - if (this is RootVisual or XamlIsland) - { - return HitTestability.Visible; - } - - // The HitTestVisibilityProperty is never set directly. This means that baseValue is always the result of the parent's CoerceHitTestVisibility. - var baseHitTestVisibility = (HitTestability)baseValue; - - // If the parent is collapsed, we should be collapsed as well. This takes priority over everything else, even if we would be visible otherwise. - if (baseHitTestVisibility == HitTestability.Collapsed) - { - return HitTestability.Collapsed; - } - - // If we're not locally hit-test visible, visible, or enabled, we should be collapsed. Our children will be collapsed as well. - // SvgElements are an exception here since they won't be loaded. - if (!(IsLoaded || HtmlTagIsSvg) || !IsHitTestVisible || Visibility != Visibility.Visible || !IsEnabledOverride()) - { - return HitTestability.Collapsed; - } - - // Special case for external html element, we are always considering them as hit testable. - if (HtmlTagIsExternallyDefined && !FeatureConfiguration.FrameworkElement.UseLegacyHitTest) - { - return HitTestability.Visible; - } - - // If we're not collapsed or invisible, we can be targeted by hit-testing. This means that we can be the source of pointer events. - if (IsViewHit()) - { - return HitTestability.Visible; - } - - // If we're not hit (usually means we don't have a Background/Fill), we're invisible. Our children will be visible or not, depending on their state. - return HitTestability.Invisible; - } - - private protected virtual void OnHitTestVisibilityChanged(HitTestability oldValue, HitTestability newValue) - { - ApplyHitTestVisibility(newValue); - } - - private void ApplyHitTestVisibility(HitTestability value) - { - // By default, elements have 'pointer-event' set to 'none' (see Uno.UI.css .uno-uielement class) - // which is aligned with HitTestVisibilityProperty's default value of Visible. - // If HitTestVisibilityProperty is calculated to Invisible or Collapsed, - // we don't want to be the target of hit-testing and raise any pointer events. - // This is done by setting 'pointer-events' to 'none'. - // However setting it to 'none' will allow pointer event to pass through the element (a.k.a. Invisible) - - WindowManagerInterop.SetPointerEvents(HtmlId, value is HitTestability.Visible); - - if (FeatureConfiguration.UIElement.AssignDOMXamlProperties) - { - UpdateDOMProperties(); - } - } - - #endregion - [TSInteropMessage(Marshaller = CodeGeneration.Disabled, UnMarshaller = CodeGeneration.Enabled)] [StructLayout(LayoutKind.Sequential, Pack = 4)] private struct NativePointerSubscriptionParams diff --git a/src/Uno.UI/UI/Xaml/UIElement.crossruntime.cs b/src/Uno.UI/UI/Xaml/UIElement.crossruntime.cs index caae2f2caf88..94c42ba352f2 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.crossruntime.cs @@ -58,7 +58,6 @@ internal void RaiseLoaded() IsLoaded = true; OnFwEltLoaded(); - UpdateHitTest(); } // Overloads for the FrameworkElement to raise the events @@ -73,7 +72,6 @@ internal void OnElementUnloaded() Depth = int.MinValue; OnFwEltUnloaded(); - UpdateHitTest(); } private void OnChildAdded(UIElement child) diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs index ae88d2c4e361..807216e3600c 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.cs @@ -507,6 +507,8 @@ protected virtual void OnVisibilityChanged(Visibility oldValue, Visibility newVi // when visibility changes. parent.InvalidateMeasure(); } + + ClearPointersStateOnVisibilityChangeIfNeeded(newVisibility); } partial void OnVisibilityChangedPartial(Visibility oldValue, Visibility newValue); @@ -688,6 +690,7 @@ private bool TryGetParentUIElementForTransformToVisual(out UIElement parentEleme protected virtual void OnIsHitTestVisibleChanged(bool oldValue, bool newValue) { OnIsHitTestVisibleChangedPartial(oldValue, newValue); + ClearPointersStateIfNeeded(); } partial void OnIsHitTestVisibleChangedPartial(bool oldValue, bool newValue); @@ -1244,7 +1247,12 @@ internal virtual void PrepareForRecycle() private partial void ClearPointerStateOnRecycle(); - internal virtual bool IsViewHit() => true; + internal virtual bool IsViewHit() +#if __WASM__ + => HtmlTagIsExternallyDefined && !FeatureConfiguration.FrameworkElement.UseLegacyHitTest; +#else + => true; +#endif internal virtual bool IsEnabledOverride() => true; diff --git a/src/Uno.UI/UI/Xaml/UIElement.macOS.cs b/src/Uno.UI/UI/Xaml/UIElement.macOS.cs index 3638103e7f4b..2325ac91a734 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.macOS.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.macOS.cs @@ -22,8 +22,6 @@ public UIElement() { Initialize(); InitializePointers(); - - UpdateHitTest(); } internal bool IsMeasureDirtyPath @@ -50,15 +48,8 @@ partial void OnOpacityChanged(DependencyPropertyChangedEventArgs args) } } - partial void OnIsHitTestVisibleChangedPartial(bool oldValue, bool newValue) - { - UpdateHitTest(); - } - partial void OnVisibilityChangedPartial(Visibility oldValue, Visibility newValue) { - UpdateHitTest(); - var isNewVisibilityHidden = newValue.IsHidden(); if (base.Hidden == isNewVisibilityHidden) diff --git a/src/Uno.UI/UI/Xaml/UIElement.reference.cs b/src/Uno.UI/UI/Xaml/UIElement.reference.cs index a7b45afae1f7..08ba2b7b7d39 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.reference.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.reference.cs @@ -33,7 +33,5 @@ public UIElement() internal UIElement ReplaceChild(int index, UIElement child) => throw new NotSupportedException("Reference assembly"); internal void ClearChildren() => throw new NotSupportedException("Reference assembly"); - - internal void UpdateHitTest() => throw new NotSupportedException("Reference assembly"); } } diff --git a/src/Uno.UI/UI/Xaml/UIElement.skia.cs b/src/Uno.UI/UI/Xaml/UIElement.skia.cs index 9adf49a518a1..294dccb82dea 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.skia.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.skia.cs @@ -43,8 +43,6 @@ public UIElement() Initialize(); InitializePointers(); - - UpdateHitTest(); } public bool UseLayoutRounding @@ -66,11 +64,6 @@ partial void OnOpacityChanged(DependencyPropertyChangedEventArgs args) ContentPresenter.UpdateNativeHostContentPresentersOpacities(); } - partial void OnIsHitTestVisibleChangedPartial(bool oldValue, bool newValue) - { - UpdateHitTest(); - } - private void UpdateOpacity() { Visual.Opacity = Visibility == Visibility.Visible ? (float)Opacity : 0; @@ -247,7 +240,6 @@ private void InnerRemoveChild(UIElement child) partial void OnVisibilityChangedPartial(Visibility oldValue, Visibility newValue) { - UpdateHitTest(); UpdateOpacity(); if (newValue == Visibility.Collapsed) diff --git a/src/Uno.UI/UI/Xaml/UIElement.wasm.cs b/src/Uno.UI/UI/Xaml/UIElement.wasm.cs index 105ce64a377c..f499d5883c6c 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.wasm.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.wasm.cs @@ -75,7 +75,6 @@ internal UIElement(string htmlTag, bool isSvg) ); InitializePointers(); - UpdateHitTest(); } ~UIElement() @@ -357,7 +356,6 @@ partial void OnUidChangedPartial() partial void OnVisibilityChangedPartial(Visibility oldValue, Visibility newValue) { InvalidateMeasure(); - UpdateHitTest(); WindowManagerInterop.SetVisibility(HtmlId, newValue == Visibility.Visible); @@ -390,8 +388,6 @@ partial void OnOpacityChanged(DependencyPropertyChangedEventArgs args) partial void OnIsHitTestVisibleChangedPartial(bool oldValue, bool newValue) { - UpdateHitTest(); - if (FeatureConfiguration.UIElement.AssignDOMXamlProperties) { UpdateDOMProperties(); @@ -403,6 +399,9 @@ partial void OnIsHitTestVisibleChangedPartial(bool oldValue, bool newValue) // algorithm is doing its layout properly. parent.InvalidateMeasure(); } + + // TODO: Do we need WindowManagerInterop.SetPointerEvents on this element and its descendants? + // TODO: Same in OnIsEnabledChanged ? } public override string ToString()