Browse Source

Track IsEffectivelyVisible state (#13972)

* add IsEffectivelyVisible test

* test IEV update

* update self and descendants

* sync from parent's isvisible and own on attach/detach

* update test

* update from review

* add comments

* remove whitespace changes

* Added some more IsEffectivelyVisible tests.

One failing.

* Rework UpdateIsEffectivelyVisible.

- Pass the parent state to `UpdateIsEffectivelyVisible`
- Remove some unneeded methods
- Update `IsEffectivelyVisible` before calling visual tree attach/detach events.

---------

Co-authored-by: Steven Kirk <[email protected]>
Jumar Macato 1 year ago
parent
commit
92e1c653a5
2 changed files with 138 additions and 19 deletions
  1. 35 19
      src/Avalonia.Base/Visual.cs
  2. 103 0
      tests/Avalonia.Base.UnitTests/VisualTests.cs

+ 35 - 19
src/Avalonia.Base/Visual.cs

@@ -195,23 +195,32 @@ namespace Avalonia
         /// <summary>
         /// Gets a value indicating whether this control and all its parents are visible.
         /// </summary>
-        public bool IsEffectivelyVisible
+        public bool IsEffectivelyVisible { get; private set; } = true;
+        
+        /// <summary>
+        /// Updates the <see cref="IsEffectivelyVisible"/> property based on the parent's
+        /// <see cref="IsEffectivelyVisible"/>.
+        /// </summary>
+        /// <param name="parentState">The effective visibility of the parent control.</param>
+        private void UpdateIsEffectivelyVisible(bool parentState)
         {
-            get
-            {
-                Visual? node = this;
+            var isEffectivelyVisible = parentState && IsVisible;
 
-                while (node != null)
-                {
-                    if (!node.IsVisible)
-                    {
-                        return false;
-                    }
+            if (IsEffectivelyVisible == isEffectivelyVisible)
+                return;
 
-                    node = node.VisualParent;
-                }
+            IsEffectivelyVisible = isEffectivelyVisible;
 
-                return true;
+            // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
+            // will cause extra allocations and overhead.
+            
+            var children = VisualChildren;
+
+            // ReSharper disable once ForCanBeConvertedToForeach
+            for (int i = 0; i < children.Count; ++i)
+            {
+                var child = children[i];
+                child.UpdateIsEffectivelyVisible(isEffectivelyVisible);
             }
         }
 
@@ -453,7 +462,11 @@ namespace Avalonia
         {
             base.OnPropertyChanged(change);
 
-            if (change.Property == FlowDirectionProperty)
+            if (change.Property == IsVisibleProperty)
+            {
+                UpdateIsEffectivelyVisible(VisualParent?.IsEffectivelyVisible ?? true);
+            } 
+            else if (change.Property == FlowDirectionProperty)
             {
                 InvalidateMirrorTransform();
 
@@ -463,7 +476,7 @@ namespace Avalonia
                 }
             }
         }
-
+ 
         protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
             base.LogicalChildrenCollectionChanged(sender, e);
@@ -492,14 +505,16 @@ namespace Avalonia
                 AttachToCompositor(compositingRenderer.Compositor);
             }
             InvalidateMirrorTransform();
+            UpdateIsEffectivelyVisible(_visualParent!.IsEffectivelyVisible);
             OnAttachedToVisualTree(e);
             AttachedToVisualTree?.Invoke(this, e);
             InvalidateVisual();
+            
             _visualRoot.Renderer.RecalculateChildren(_visualParent!);
-
-            if (ZIndex != 0 && VisualParent is Visual parent)
-                parent.HasNonUniformZIndexChildren = true;
-
+            
+            if (ZIndex != 0 && _visualParent is { })
+                _visualParent.HasNonUniformZIndexChildren = true;
+            
             var visualChildren = VisualChildren;
             var visualChildrenCount = visualChildren.Count;
 
@@ -529,6 +544,7 @@ namespace Avalonia
             }
 
             DisableTransitions();
+            UpdateIsEffectivelyVisible(true);
             OnDetachedFromVisualTree(e);
             DetachFromCompositor();
 

+ 103 - 0
tests/Avalonia.Base.UnitTests/VisualTests.cs

@@ -349,5 +349,108 @@ namespace Avalonia.Base.UnitTests
 
             renderer.Verify(x => x.RecalculateChildren(stackPanel));
         }
+
+        [Theory]
+        [InlineData(new[] { 1, 2, 3 }, true, true, true, true, true, true)]
+        [InlineData(new[] { 3, 2, 1 }, true, true, true, true, true, true)]
+        [InlineData(new[] { 1 }, false, true, true, false, false, false)]
+        [InlineData(new[] { 2 }, true, false, true, true, false, false)]
+        [InlineData(new[] { 3 }, true, true, false, true, true, false)]
+        [InlineData(new[] { 3, 1}, true, true, false, true, true, false)]
+        
+        [InlineData(new[] { 2, 3, 1 }, true, false, true, true, false, false, true)]
+        [InlineData(new[] { 3, 1, 2 }, true, true, false, true, true, false, true)]
+        [InlineData(new[] { 3, 2, 1 }, true, true, false, true, true, false, true)]
+
+        public void IsEffectivelyVisible_Propagates_To_Visual_Children(int[] assignOrder, bool rootV, bool child1V,
+            bool child2V, bool rootExpected, bool child1Expected, bool child2Expected, bool initialSetToFalse = false)
+        {
+            var child2 = new Decorator();
+            var child1 = new Decorator { Child = child2 };
+            var root = new TestRoot { Child = child1 };
+
+            Assert.True(child2.IsEffectivelyVisible);
+
+            if (initialSetToFalse)
+            {
+                root.IsVisible = false;
+                child1.IsVisible = false;
+                child2.IsVisible = false;
+            }
+
+            foreach (var order in assignOrder)
+            {
+                switch (order)
+                {
+                    case 1:
+                        root.IsVisible = rootV;
+                        break;
+                    case 2:
+                        child1.IsVisible = child1V;
+                        break;
+                    case 3:
+                        child2.IsVisible = child2V;
+                        break;
+                }
+            }
+
+            Assert.Equal(rootExpected, root.IsEffectivelyVisible);
+            Assert.Equal(child1Expected, child1.IsEffectivelyVisible);
+            Assert.Equal(child2Expected, child2.IsEffectivelyVisible);
+        }
+
+        [Fact]
+        public void Added_Child_Has_Correct_IsEffectivelyVisible()
+        {
+            var root = new TestRoot { IsVisible = false };
+            var child = new Decorator();
+
+            root.Child = child;
+            Assert.False(child.IsEffectivelyVisible);
+        }
+
+        [Fact]
+        public void Added_Grandchild_Has_Correct_IsEffectivelyVisible()
+        {
+            var child = new Decorator();
+            var grandchild = new Decorator();
+            var root = new TestRoot 
+            { 
+                IsVisible = false,
+                Child = child
+            };
+
+            child.Child = grandchild;
+            Assert.False(grandchild.IsEffectivelyVisible);
+        }
+
+        [Fact]
+        public void Removing_Child_Resets_IsEffectivelyVisible()
+        {
+            var child = new Decorator();
+            var root = new TestRoot { Child = child, IsVisible = false };
+
+            Assert.False(child.IsEffectivelyVisible);
+
+            root.Child = null;
+
+            Assert.True(child.IsEffectivelyVisible);
+        }
+
+        [Fact]
+        public void Removing_Child_Resets_IsEffectivelyVisible_Of_Grandchild()
+        {
+            var grandchild = new Decorator();
+            var child = new Decorator { Child = grandchild };
+            var root = new TestRoot { Child = child, IsVisible = false };
+
+            Assert.False(child.IsEffectivelyVisible);
+            Assert.False(grandchild.IsEffectivelyVisible);
+
+            root.Child = null;
+
+            Assert.True(child.IsEffectivelyVisible);
+            Assert.True(grandchild.IsEffectivelyVisible);
+        }
     }
 }