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)