diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml index 97f5c4082b2f..58d366cb0537 100644 --- a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml @@ -18,9 +18,8 @@ Deepness=, Wideness=, Iterations= - - Use MEASURE_DIRTY_PATH - + Use MEASURE_DIRTY_PATH + Use ARRANGE_DIRTY_PATH diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml.cs index 19edbc09504c..27939cbde9c9 100644 --- a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml.cs +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml/UIElementTests/UIElement_MeasurePerf.xaml.cs @@ -23,15 +23,18 @@ public UIElement_MeasurePerf() #if !NETFX_CORE bool originalUseInvalidateMeasurePath = FeatureConfiguration.UIElement.UseInvalidateMeasurePath; + bool originalUseInvalidateArrangePath = FeatureConfiguration.UIElement.UseInvalidateArrangePath; Loaded += (_, _) => { optimizeMeasure.IsChecked = FeatureConfiguration.UIElement.UseInvalidateMeasurePath; + optimizeArrange.IsChecked = FeatureConfiguration.UIElement.UseInvalidateArrangePath; }; Unloaded += (_, _) => { FeatureConfiguration.UIElement.UseInvalidateMeasurePath = originalUseInvalidateMeasurePath; + FeatureConfiguration.UIElement.UseInvalidateArrangePath = originalUseInvalidateArrangePath; }; #endif } @@ -197,6 +200,15 @@ private void changeOptimizeMeasure(object sender, RoutedEventArgs e) { FeatureConfiguration.UIElement.UseInvalidateMeasurePath = false; } + + if (optimizeArrange.IsChecked is true) + { + FeatureConfiguration.UIElement.UseInvalidateArrangePath = true; + } + else + { + FeatureConfiguration.UIElement.UseInvalidateArrangePath = false; + } #endif } } diff --git a/src/Uno.UI/Extensions/ViewExtensions.cs b/src/Uno.UI/Extensions/ViewExtensions.cs index 48b8a2cc235b..1c2a8724256c 100644 --- a/src/Uno.UI/Extensions/ViewExtensions.cs +++ b/src/Uno.UI/Extensions/ViewExtensions.cs @@ -107,6 +107,7 @@ internal static string GetLayoutDetails(this UIElement uiElement) #else #endif .Append(uiElement.IsMeasureDirtyPathDisabled is true ? " MEASURE_DIRTY_PATH_DISABLED" : "") + .Append(uiElement.IsArrangeDirtyPathDisabled is true ? " ARRANGE_DIRTY_PATH_DISABLED" : "") .Append(uiElement.IsArrangeDirty is true ? " ARRANGE_DIRTY" : ""); return sb.ToString(); diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs index 7de3e0b96b39..f575e26a7f19 100644 --- a/src/Uno.UI/FeatureConfiguration.cs +++ b/src/Uno.UI/FeatureConfiguration.cs @@ -514,10 +514,16 @@ public static class UIElement { /// /// Call the .MeasureOverride only on element explicitly invalidating - /// their measure and when the size changed. + /// their measure and when the available size is changing. /// public static bool UseInvalidateMeasurePath { get; set; } = true; + /// + /// Call the .ArrangeOverride only on element explicitly invalidating + /// their arrange and when the final rect is changing. + /// + public static bool UseInvalidateArrangePath { get; set; } = true; + /// /// [DEPRECATED] /// Not used anymore, does nothing. diff --git a/src/Uno.UI/UI/Xaml/FrameworkElementHelper.cs b/src/Uno.UI/UI/Xaml/FrameworkElementHelper.cs index ab8e1f16a49c..3084a6408427 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElementHelper.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElementHelper.cs @@ -97,5 +97,38 @@ public static void SetUseMeasurePathDisabled(UIElement element, bool state = tru public static bool GetUseMeasurePathDisabled(FrameworkElement element) => element.IsMeasureDirtyPathDisabled; + + /// + /// This is the equivalent of + /// but just for a specific element (and its descendants) in the visual tree. + /// + /// + /// This will have no effect if + /// is set to false. + /// + public static void SetUseArrangePathDisabled(UIElement element, bool state = true, bool eager = true, bool invalidate = true) + { + element.IsArrangeDirtyPathDisabled = state; + + if (eager) + { + using var children = element.GetChildren().GetEnumerator(); + while (children.MoveNext()) + { + if (children.Current is FrameworkElement child) + { + SetUseArrangePathDisabled(child, state, eager: true, invalidate); + } + } + } + + if (invalidate) + { + element.InvalidateArrange(); + } + } + + public static bool GetUseArrangePathDisabled(FrameworkElement element) + => element.IsArrangeDirtyPathDisabled; } } diff --git a/src/Uno.UI/UI/Xaml/ILayouterElement.cs b/src/Uno.UI/UI/Xaml/ILayouterElement.cs index 67c55d2e57b4..e53853c00b5f 100644 --- a/src/Uno.UI/UI/Xaml/ILayouterElement.cs +++ b/src/Uno.UI/UI/Xaml/ILayouterElement.cs @@ -104,7 +104,7 @@ internal static bool DoMeasure(this ILayouterElement element, Size availableSize // If the child is dirty (or is a path to a dirty descendant child), // We're remeasuring it. - if (child is UIElement { IsMeasureOrMeasureDirtyPath: true }) + if (child is UIElement { IsMeasureDirtyOrMeasureDirtyPath: true }) { var previousDesiredSize = LayoutInformation.GetDesiredSize(child); element.Layouter.MeasureChild(child, LayoutInformation.GetAvailableSize(child)); diff --git a/src/Uno.UI/UI/Xaml/UIElement.Layout.cs b/src/Uno.UI/UI/Xaml/UIElement.Layout.cs index 44c140ce9e6e..279b86624044 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Layout.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Layout.cs @@ -2,6 +2,7 @@ // "Managed Measure Dirty Path" means it's the responsibility of the // managed code (Uno) to walk the tree and do the measure phase. #define IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH +#define IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH #endif using System; using System.Runtime.CompilerServices; @@ -31,7 +32,7 @@ internal bool IsMeasureDirtyPath #endif #if IMPLEMENTS_MANAGED_MEASURE_DIRTY_PATH - internal bool IsMeasureOrMeasureDirtyPath + internal bool IsMeasureDirtyOrMeasureDirtyPath { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => IsAnyLayoutFlagsSet(LayoutFlag.MeasureDirty | LayoutFlag.MeasureDirtyPath); @@ -40,7 +41,7 @@ internal bool IsMeasureOrMeasureDirtyPath /// /// This is for compatibility - not implemented yet on this platform /// - internal bool IsMeasureOrMeasureDirtyPath + internal bool IsMeasureDirtyOrMeasureDirtyPath { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => IsMeasureDirty || IsMeasureDirtyPath; @@ -68,6 +69,29 @@ internal bool IsArrangeDirty } #endif +#if IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH + internal bool IsArrangeDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsAnyLayoutFlagsSet(LayoutFlag.ArrangeDirtyPath); + } + + internal bool IsArrangeDirtyOrArrangeDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsAnyLayoutFlagsSet(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + } +#else + /// + /// This is for compatibility - not implemented yet on this platform + /// + internal bool IsArrangeDirtyOrArrangeDirtyPath + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsArrangeDirty || IsArrangeDirtyPath; + } +#endif + [Flags] internal enum LayoutFlag : byte { @@ -84,7 +108,7 @@ internal enum LayoutFlag : byte #endif /// - /// Indicated the first measure has been done on the element after been connected to parent + /// Indicates that first measure has been done on the element after been connected to parent /// FirstMeasureDone = 0b0000_0100, @@ -104,7 +128,27 @@ internal enum LayoutFlag : byte ArrangeDirty = 0b0001_0000, #endif - // ArrangeDirtyPath not implemented yet +#if IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH + /// + /// Means the Arrange is dirty on at least one child of this element + /// + ArrangeDirtyPath = 0b0010_0000, + + /// + /// Indicates that first arrange has been done on the element and we can use the + /// LayoutInformation.GetLayoutSlot() to get previous finalRect + /// + FirstArrangeDone = 0b0100_0000, +#endif + + /// + /// Means the MeasureDirtyPath is disabled on this element. + /// + /// + /// This flag is copied to children when they are attached, but can be re-enabled afterwards. + /// This flag is used during invalidation + /// + ArrangeDirtyPathDisabled = 0b1000_0000, } private const LayoutFlag DEFAULT_STARTING_LAYOUTFLAGS = 0; @@ -115,6 +159,10 @@ internal enum LayoutFlag : byte #endif #if !__ANDROID__ LayoutFlag.ArrangeDirty | +#endif +#if IMPLEMENTS_MANAGED_ARRANGE_DIRTY_PATH + LayoutFlag.ArrangeDirtyPath | + LayoutFlag.FirstArrangeDone | #endif LayoutFlag.FirstMeasureDone; @@ -172,6 +220,13 @@ internal bool IsMeasureDirtyPathDisabled set => SetLayoutFlags(LayoutFlag.MeasureDirtyPathDisabled, value); } + internal bool IsArrangeDirtyPathDisabled + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => IsLayoutFlagSet(LayoutFlag.ArrangeDirtyPathDisabled); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => SetLayoutFlags(LayoutFlag.ArrangeDirtyPathDisabled, value); + } } } diff --git a/src/Uno.UI/UI/Xaml/UIElement.Layout.netstd.cs b/src/Uno.UI/UI/Xaml/UIElement.Layout.netstd.cs index f6fdfe7d8aee..13e56afb836d 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Layout.netstd.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Layout.netstd.cs @@ -36,7 +36,7 @@ public void InvalidateMeasure() if (FeatureConfiguration.UIElement.UseInvalidateMeasurePath && !IsMeasureDirtyPathDisabled) { - InvalidateParentMeasurePath(); + InvalidateParentMeasureDirtyPath(); } else { @@ -49,24 +49,24 @@ public void InvalidateMeasure() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InvalidateMeasurePath() + private void InvalidateMeasureDirtyPath() { - if(IsMeasureOrMeasureDirtyPath) + if(IsMeasureDirtyOrMeasureDirtyPath) { return; // Already invalidated } SetLayoutFlags(LayoutFlag.MeasureDirtyPath); - InvalidateParentMeasurePath(); + InvalidateParentMeasureDirtyPath(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void InvalidateParentMeasurePath() + internal void InvalidateParentMeasureDirtyPath() { if (this.GetParent() is UIElement parent) { - parent.InvalidateMeasurePath(); + parent.InvalidateMeasureDirtyPath(); } else if (IsVisualTreeRoot) { @@ -87,9 +87,39 @@ public void InvalidateArrange() } SetLayoutFlags(LayoutFlag.ArrangeDirty); + + if (FeatureConfiguration.UIElement.UseInvalidateArrangePath && !IsArrangeDirtyPathDisabled) + { + InvalidateParentArrangeDirtyPath(); + } + else + { + (this.GetParent() as UIElement)?.InvalidateArrange(); + if (IsVisualTreeRoot) + { + Window.InvalidateArrange(); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InvalidateArrangeDirtyPath() + { + if (IsArrangeDirtyOrArrangeDirtyPath) + { + return; // Already invalidated + } + + SetLayoutFlags(LayoutFlag.ArrangeDirtyPath); + + InvalidateParentArrangeDirtyPath(); + } + + private void InvalidateParentArrangeDirtyPath() + { if (this.GetParent() is UIElement parent) { - parent.InvalidateArrange(); + parent.InvalidateArrangeDirtyPath(); } else { @@ -218,7 +248,7 @@ private void DoMeasure(Size availableSize) //foreach (var child in children) while(children.MoveNext()) { - if (children.Current is { IsMeasureOrMeasureDirtyPath: true } child) + if (children.Current is { IsMeasureDirtyOrMeasureDirtyPath: true } child) { // If the child is dirty (or is a path to a dirty descendant child), // We're remeasuring it. @@ -268,7 +298,7 @@ public void Arrange(Rect finalRect) return; } - if (!IsArrangeDirty && finalRect == LayoutSlot) + if (!IsArrangeDirtyOrArrangeDirtyPath && finalRect == LayoutSlot) { return; // Calling Arrange would be a waste of CPU time here. } @@ -304,17 +334,91 @@ private void ArrangeVisualTreeRoot(Rect finalRect) private void DoArrange(Rect finalRect) { - ShowVisual(); + var isFirstArrange = !IsLayoutFlagSet(LayoutFlag.FirstArrangeDone); + + var isDirty = + isFirstArrange + || IsArrangeDirty + || finalRect != LayoutSlot; + + if (!isDirty && !IsArrangeDirtyPath) + { + return; // Nothing do to + } + + if (IsMeasureDirtyOrMeasureDirtyPath) + { + return; // Measure in invalidated: too soon to arrange. + } + + if (isFirstArrange) + { + SetLayoutFlags(LayoutFlag.FirstArrangeDone); + } - // We must store the updated slot before natively arranging the element, - // so the updated value can be read by indirect code that is being invoked on arrange. - // For instance, the EffectiveViewPort computation reads that value to detect slot changes (cf. PropagateEffectiveViewportChange) - LayoutInformation.SetLayoutSlot(this, finalRect); + var remainingTries = MaxLayoutIterations; + + while (--remainingTries > 0) + { + if (IsMeasureDirtyOrMeasureDirtyPath) + { + return; // Measure in invalidated: too soon to arrange. + } + + if (isDirty) + { + ShowVisual(); + + // We must store the updated slot before natively arranging the element, + // so the updated value can be read by indirect code that is being invoked on arrange. + // For instance, the EffectiveViewPort computation reads that value to detect slot changes (cf. PropagateEffectiveViewportChange) + LayoutInformation.SetLayoutSlot(this, finalRect); + + // We must reset the flag **BEFORE** doing the actual arrange, so the elements are able to re-invalidate themselves + ClearLayoutFlags(LayoutFlag.ArrangeDirty | LayoutFlag.ArrangeDirtyPath); + + ArrangeCore(finalRect); + + SetLayoutFlags(LayoutFlag.FirstArrangeDone); + + break; + } + else if (IsArrangeDirtyPath) + { + ClearLayoutFlags(LayoutFlag.ArrangeDirtyPath); + + var children = GetChildren().GetEnumerator(); + + while (children.MoveNext()) + { + var child = children.Current; + + if (child is { IsArrangeDirtyOrArrangeDirtyPath: true }) + { + var previousRenderSize = child.RenderSize; + child.Arrange(child.LayoutSlot); + + if (child.RenderSize != previousRenderSize) + { + isDirty = true; + break; + } + } + } - // We must reset the flag **BEFORE** doing the actual arrange, so the elements are able to re-invalidate themselves - ClearLayoutFlags(LayoutFlag.ArrangeDirty); + children.Dispose(); // no "using" operator here to prevent an implicit try-catch on Wasm + + if (!isDirty) + { + break; + } + } + else + { + break; + } + } - ArrangeCore(finalRect); } partial void HideVisual(); diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs index f479e6bb747d..2223b72164c8 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.cs @@ -541,7 +541,7 @@ private static void InnerUpdateLayout(UIElement root) for (var i = 0; i < MaxLayoutIterations; i++) { // On Android, Measure and arrange are the same - if (root.IsMeasureOrMeasureDirtyPath) + if (root.IsMeasureDirtyOrMeasureDirtyPath) { root.Measure(bounds.Size); root.Arrange(bounds); @@ -554,7 +554,7 @@ private static void InnerUpdateLayout(UIElement root) #else for (var i = 0; i < MaxLayoutIterations; i++) { - if (root.IsMeasureOrMeasureDirtyPath) + if (root.IsMeasureDirtyOrMeasureDirtyPath) { root.Measure(bounds.Size); } diff --git a/src/Uno.UI/UI/Xaml/UIElement.skia.cs b/src/Uno.UI/UI/Xaml/UIElement.skia.cs index 74e5c3dda763..1518987611bf 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.skia.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.skia.cs @@ -143,13 +143,19 @@ public void AddChild(UIElement child, int? index = null) child.InvalidateMeasure(); } - if (child.IsArrangeDirty && !IsArrangeDirty) + if (IsArrangeDirtyPathDisabled) { - InvalidateArrange(); + FrameworkElementHelper.SetUseArrangePathDisabled(child); // will invalidate too + } + else if (child.IsArrangeDirty && !IsArrangeDirty) + { + child.InvalidateArrange(); } // Force a new measure of this element (the parent of the new child) InvalidateMeasure(); + InvalidateArrange(); + } internal void MoveChildTo(int oldIndex, int newIndex)