diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 9774468e163..85dc082ff6c 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -195,23 +195,32 @@ public Geometry? Clip /// /// Gets a value indicating whether this control and all its parents are visible. /// - public bool IsEffectivelyVisible + public bool IsEffectivelyVisible { get; private set; } = true; + + /// + /// Updates the property based on the parent's + /// . + /// + /// The effective visibility of the parent control. + 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 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { 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 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } } - + protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); @@ -492,14 +505,16 @@ protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs 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 @@ protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArg } DisableTransitions(); + UpdateIsEffectivelyVisible(true); OnDetachedFromVisualTree(e); DetachFromCompositor(); diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs index 0150a067152..044793007d2 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs @@ -349,5 +349,108 @@ public void Changing_ZIndex_Should_Recalculate_Parent_Children() 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); + } } }