Browse Source

Merge pull request #11127 from AvaloniaUI/fixes/11076-layout-invalidation

Fix layout invalidation when controls made effectively visible.
Max Katz 2 years ago
parent
commit
9f64bad365

+ 1 - 1
src/Avalonia.Base/Layout/LayoutManager.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Layout
     /// </summary>
     public class LayoutManager : ILayoutManager, IDisposable
     {
-        private const int MaxPasses = 3;
+        private const int MaxPasses = 10;
         private readonly Layoutable _owner;
         private readonly LayoutQueue<Layoutable> _toMeasure = new LayoutQueue<Layoutable>(v => !v.IsMeasureValid);
         private readonly LayoutQueue<Layoutable> _toArrange = new LayoutQueue<Layoutable>(v => !v.IsArrangeValid);

+ 15 - 3
src/Avalonia.Base/Layout/LayoutQueue.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using Avalonia.Logging;
 
 namespace Avalonia.Layout
 {
@@ -48,10 +49,21 @@ namespace Avalonia.Layout
         {
             _loopQueueInfo.TryGetValue(item, out var info);
 
-            if (!info.Active && info.Count < _maxEnqueueCountPerLoop)
+            if (!info.Active)
             {
-                _inner.Enqueue(item);
-                _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
+                if (info.Count < _maxEnqueueCountPerLoop)
+                {
+                    _inner.Enqueue(item);
+                    _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
+                }
+                else
+                {
+                    Logger.TryGet(LogEventLevel.Warning, LogArea.Layout)?.Log(
+                        this,
+                        "Layout cycle detected. Item {Item} was enqueued {Count} times.",
+                        item,
+                        info.Count);
+                }
             }
         }
 

+ 39 - 1
src/Avalonia.Base/Layout/Layoutable.cs

@@ -776,10 +776,24 @@ namespace Avalonia.Layout
                 // All changes to visibility cause the parent element to be notified.
                 this.GetVisualParent<Layoutable>()?.ChildDesiredSizeChanged(this);
 
-                // We only invalidate outselves when visibility is changed to true.
                 if (change.GetNewValue<bool>())
                 {
+                    // We only invalidate ourselves when visibility is changed to true.
                     InvalidateMeasure();
+
+                    // If any descendant had its measure/arrange invalidated while we were hidden,
+                    // they will need to to be registered with the layout manager now that they
+                    // are again effectively visible. If IsEffectivelyVisible becomes an observable
+                    // property then we can piggy-pack on that; for the moment we do this manually.
+                    if (VisualRoot is ILayoutRoot layoutRoot)
+                    {
+                        var count = VisualChildren.Count;
+
+                        for (var i = 0; i < count; ++i)
+                        {
+                            (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutRoot.LayoutManager);
+                        }
+                    }
                 }
             }
         }
@@ -804,6 +818,30 @@ namespace Avalonia.Layout
             InvalidateMeasure();
         }
 
+        private void AncestorBecameVisible(ILayoutManager layoutManager)
+        {
+            if (!IsVisible)
+                return;
+
+            if (!IsMeasureValid)
+            {
+                layoutManager.InvalidateMeasure(this);
+                InvalidateVisual();
+            }
+            else if (!IsArrangeValid)
+            {
+                layoutManager.InvalidateArrange(this);
+                InvalidateVisual();
+            }
+
+            var count = VisualChildren.Count;
+
+            for (var i = 0; i < count; ++i)
+            {
+                (VisualChildren[i] as Layoutable)?.AncestorBecameVisible(layoutManager);
+            }
+        }
+
         /// <summary>
         /// Called when the layout manager raises a LayoutUpdated event.
         /// </summary>

+ 0 - 34
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -565,7 +565,6 @@ namespace Avalonia.Controls
                 GetItemIsOwnContainer(items, index) ??
                 GetRecycledElement(items, index) ??
                 CreateElement(items, index);
-            InvalidateHack(e);
             return e;
         }
 
@@ -713,39 +712,6 @@ namespace Avalonia.Controls
             }
         }
 
-        private static void InvalidateHack(Control c)
-        {
-            bool HasInvalidations(Control c)
-            {
-                if (!c.IsMeasureValid)
-                    return true;
-
-                for (var i = 0; i < c.VisualChildren.Count; ++i)
-                {
-                    if (c.VisualChildren[i] is Control child)
-                    {
-                        if (!child.IsMeasureValid || HasInvalidations(child))
-                            return true;
-                    }
-                }
-
-                return false;
-            }
-
-            void Invalidate(Control c)
-            {
-                c.InvalidateMeasure();
-                for (var i = 0; i < c.VisualChildren.Count; ++i)
-                {
-                    if (c.VisualChildren[i] is Control child)
-                        Invalidate(child);
-                }
-            }
-
-            if (HasInvalidations(c))
-                Invalidate(c);
-        }
-
         private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
         {
             if (_unrealizedFocusedElement is null || sender != _unrealizedFocusedElement)

+ 23 - 0
tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs

@@ -59,6 +59,29 @@ namespace Avalonia.Base.UnitTests.Layout
             Assert.False(control.Arranged);
         }
 
+        [Fact]
+        public void Lays_Out_Descendents_That_Were_Invalidated_While_Ancestor_Was_Not_Visible()
+        {
+            // Issue #11076
+            var control = new LayoutTestControl();
+            var parent = new Decorator { Child = control };
+            var grandparent = new Decorator { Child = parent };
+            var root = new LayoutTestRoot { Child = grandparent };
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            grandparent.IsVisible = false;
+            control.InvalidateMeasure();
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            grandparent.IsVisible = true;
+
+            root.LayoutManager.ExecuteLayoutPass();
+            
+            Assert.True(control.IsMeasureValid);
+            Assert.True(control.IsArrangeValid);
+        }
+
         [Fact]
         public void Arranges_InvalidateArranged_Control()
         {