diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs index fdcfd0f28918..f99ea4764776 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.Measure.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -7,9 +8,11 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; -using Uno.UI.RuntimeTests.Extensions; -using System.Collections.ObjectModel; using Windows.Foundation; +using Uno.UI.Helpers; +using Uno.UI.RuntimeTests.Extensions; +using Uno.UI.RuntimeTests.Helpers; + #if WINAPPSDK using Uno.UI.Extensions; #elif __IOS__ @@ -442,6 +445,44 @@ public async Task When_ItemsSource_INCC_StartTwo_RemoveOne() await WindowHelper.WaitFor(() => Math.Abs(SUT.ActualHeight - 29) <= Epsilon, message: $"ListView failed to shrink from removing item: (ActualHeight: {SUT.ActualHeight})"); } +#if __IOS__ || __ANDROID__ + [TestMethod] + [RunsOnUIThread] + public async Task When_Item_With_NegativeMargin_AsdAsd() // todo@xy: remove asdasd + { + const string PathData = "M 2 2 H 18 V 18 H 2 Z M 3 3 V 17 H 17 V 3 Z"; // 18x18 square with 1px border located at 2,2 + var setup = new ListView + { + ItemsSource = Enumerable.Range(0, 1).ToArray(), + ItemTemplate = XamlHelper.LoadXaml($$""" + + + + + + + + """), + }; + + await UITestHelper.Load(setup); + + // We can't really test rotating the device in the context of runtime tests. But still, this is a good repro. + // When loaded, we should see a red square, whose lower half is sitting in a pink background. + // When you rotate the device (portrait <-> landscape), the red square should still be fully visible. + + // Previously, there is a bug where rotating could cause the view with negative padding to be clipped. + // The visual hierarchy needs a special setup, it is just the easiest to replicate with a ListView... + + // It can be verified by checking `.Layer.Mask` remains consistent for the visual tree under ListView, + // before and after the device rotation. In this case, it was the SutBorder that magically had a `.Layer.Mask` assigned. + } +#endif + // Works around ScrollIntoView() not implemented for all platforms private static void ScrollTo(ListViewBase listViewBase, double vOffset) { diff --git a/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.iOSmacOS.cs index 110301f0569d..5925cb5f2600 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.iOSmacOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Border/BorderLayerRenderer.iOSmacOS.cs @@ -427,13 +427,19 @@ GradientBrush gradientBorder when gradientBorder.CanApplyToBorder(cornerRadius) sublayers.Add(backgroundLayer); parent.InsertSublayer(backgroundLayer, insertionIndex); - parent.Mask = new CAShapeLayer() + // Normally, the parent.Mask (or owner.Layer.Mask) is usually cleared by ApplyNativeClip that follows. + // However, on device rotation (portrait <-> landscape), ApplyNativeClip happens before this method, causing the view to have an unwanted clipping. + // Here, we are reusing the logics of ApplyNativeClip to decide when to not apply the mask (as it would be cleared anyways). + if (owner.GetNativeClippedViewport().IsFinite) { - Path = outerPath, - Frame = area, - // We only use the fill color to create the mask area - FillColor = _Color.White.CGColor, - }; + parent.Mask = new CAShapeLayer() + { + Path = outerPath, + Frame = area, + // We only use the fill color to create the mask area + FillColor = _Color.White.CGColor, + }; + } updatedBoundsPath = outerPath; } diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs index 324d88df7508..c2a0ba2d5e8d 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.cs @@ -931,6 +931,16 @@ internal void ApplyClip() #elif __WASM__ InvalidateArrange(); #else + var rect = GetNativeClippedViewport(); + + ApplyNativeClip(rect); + OnViewportUpdated(rect); +#endif + } + +#if !(__SKIA__ || __WASM__) + internal Rect GetNativeClippedViewport() + { Rect rect; if (Clip == null) @@ -957,10 +967,9 @@ internal void ApplyClip() } } - ApplyNativeClip(rect); - OnViewportUpdated(rect); -#endif + return rect; } +#endif partial void ApplyNativeClip(Rect rect #if __SKIA__ diff --git a/src/Uno.UI/UI/Xaml/UIElement.iOS.cs b/src/Uno.UI/UI/Xaml/UIElement.iOS.cs index 2377133ccfad..332e98710c6d 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.iOS.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.iOS.cs @@ -63,12 +63,7 @@ internal bool IsArrangeDirtyPath partial void ApplyNativeClip(Rect rect) { - if (rect.IsEmpty - || double.IsPositiveInfinity(rect.X) - || double.IsPositiveInfinity(rect.Y) - || double.IsPositiveInfinity(rect.Width) - || double.IsPositiveInfinity(rect.Height) - ) + if (!rect.IsFinite) { if (!ClippingIsSetByCornerRadius) {