diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 0f365fcb08..a4c674a03b 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Add: controls = e.NewItems.OfType().ToList(); LogicalChildren.InsertRange(e.NewStartingIndex, controls); - VisualChildren.AddRange(e.NewItems.OfType()); + VisualChildren.InsertRange(e.NewStartingIndex, e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Move: diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index b4aa89d5bf..38c29289b6 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -568,6 +568,28 @@ namespace Avalonia }); } + protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + SetLogicalParent(e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Remove: + ClearLogicalParent(e.OldItems.Cast()); + break; + + case NotifyCollectionChangedAction.Replace: + ClearLogicalParent(e.OldItems.Cast()); + SetLogicalParent(e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); + } + } + /// /// Called when the styled element is added to a rooted logical tree. /// @@ -736,28 +758,6 @@ namespace Avalonia OnDataContextChanged(EventArgs.Empty); } - private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - SetLogicalParent(e.NewItems.Cast()); - break; - - case NotifyCollectionChangedAction.Remove: - ClearLogicalParent(e.OldItems.Cast()); - break; - - case NotifyCollectionChangedAction.Replace: - ClearLogicalParent(e.OldItems.Cast()); - SetLogicalParent(e.NewItems.Cast()); - break; - - case NotifyCollectionChangedAction.Reset: - throw new NotSupportedException("Reset should not be signaled on LogicalChildren collection"); - } - } - private void SetLogicalParent(IEnumerable children) { foreach (var i in children) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index ebaf62b2c0..bf1799bbdc 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -30,6 +30,7 @@ namespace Avalonia.Rendering private bool _disposed; private volatile IRef _scene; private DirtyVisuals _dirty; + private HashSet _recalculateChildren; private IRef _overlay; private int _lastSceneId = -1; private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); @@ -135,6 +136,8 @@ namespace Avalonia.Rendering DisposeRenderTarget(); } + public void RecalculateChildren(IVisual visual) => _recalculateChildren?.Add(visual); + void DisposeRenderTarget() { using (var l = _lock.TryLock()) @@ -229,6 +232,8 @@ namespace Avalonia.Rendering internal void UnitTestRender() => Render(false); + internal Scene UnitTestScene() => _scene.Item; + private void Render(bool forceComposite) { using (var l = _lock.TryLock()) @@ -516,10 +521,19 @@ namespace Avalonia.Rendering if (_dirty == null) { _dirty = new DirtyVisuals(); + _recalculateChildren = new HashSet(); _sceneBuilder.UpdateAll(scene); } - else if (_dirty.Count > 0) + else { + foreach (var visual in _recalculateChildren) + { + var node = scene.FindNode(visual); + ((VisualNode)node)?.SortChildren(scene); + } + + _recalculateChildren.Clear(); + foreach (var visual in _dirty) { _sceneBuilder.Update(scene, visual); diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 36a1f7d220..9ad7186dca 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -50,6 +50,12 @@ namespace Avalonia.Rendering /// The visuals at the specified point, topmost first. IEnumerable HitTest(Point p, IVisual root, Func filter); + /// + /// Informs the renderer that the z-ordering of a visual's children has changed. + /// + /// The visual. + void RecalculateChildren(IVisual visual); + /// /// Called when a resize notification is received by the control being rendered. /// diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 21129e38af..b2d242d4af 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -163,6 +163,9 @@ namespace Avalonia.Rendering return HitTest(root, p, filter); } + /// + public void RecalculateChildren(IVisual visual) => AddDirty(visual); + /// public void Start() { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 12cfc7cbe3..709a935450 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -172,6 +172,37 @@ namespace Avalonia.Rendering.SceneGraph old.Dispose(); } + /// + /// Sorts the collection according to the order of the visual's + /// children and their z-index. + /// + /// The scene that the node is a part of. + public void SortChildren(Scene scene) + { + var keys = new List(); + + for (var i = 0; i < Visual.VisualChildren.Count; ++i) + { + var child = Visual.VisualChildren[i]; + var zIndex = child.ZIndex; + keys.Add(((long)zIndex << 32) + i); + } + + keys.Sort(); + _children.Clear(); + + foreach (var i in keys) + { + var child = Visual.VisualChildren[(int)(i & 0xffffffff)]; + var node = scene.FindNode(child); + + if (node != null) + { + _children.Add(node); + } + } + } + /// /// Removes items in the collection from the specified index /// to the end. diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 9e088cb136..1f2d67b69e 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -111,6 +111,7 @@ namespace Avalonia IsVisibleProperty, OpacityProperty); RenderTransformProperty.Changed.Subscribe(RenderTransformChanged); + ZIndexProperty.Changed.Subscribe(ZIndexChanged); } /// @@ -345,6 +346,12 @@ namespace Avalonia } } + protected override void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.LogicalChildrenCollectionChanged(sender, e); + VisualRoot?.Renderer?.RecalculateChildren(this); + } + /// /// Calls the method /// for this control and all of its visual descendants. @@ -501,6 +508,18 @@ namespace Avalonia } } + /// + /// Called when the property changes on any control. + /// + /// The event args. + private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) + { + var sender = e.Sender as IVisual; + var parent = sender?.VisualParent; + sender?.InvalidateVisual(); + parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); + } + /// /// Called when the 's event /// is fired. diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index a841174d2d..1da4746516 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -401,6 +401,10 @@ namespace Avalonia.LeakTests { } + public void RecalculateChildren(IVisual visual) + { + } + public void Resized(Size size) { } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index f094d9c78d..4c302a24a2 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -96,6 +96,180 @@ namespace Avalonia.Visuals.UnitTests.Rendering Assert.Equal(new List { root, decorator, border, canvas }, result); } + [Fact] + public void Should_Update_VisualNode_Order_On_Child_Remove_Insert() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children= + { + (canvas1 = new Canvas()), + (canvas2 = new Canvas()), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + stack.Children.Remove(canvas2); + stack.Children.Insert(0, canvas2); + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + + [Fact] + public void Should_Update_VisualNode_Order_On_Child_Move() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children = + { + (canvas1 = new Canvas()), + (canvas2 = new Canvas()), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + stack.Children.Move(1, 0); + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + + [Fact] + public void Should_Update_VisualNode_Order_On_ZIndex_Change() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children = + { + (canvas1 = new Canvas { ZIndex = 1 }), + (canvas2 = new Canvas { ZIndex = 2 }), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + canvas1.ZIndex = 3; + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + + [Fact] + public void Should_Update_VisualNode_Order_On_ZIndex_Change_With_Dirty_Ancestor() + { + var dispatcher = new ImmediateDispatcher(); + var loop = new Mock(); + + StackPanel stack; + Canvas canvas1; + Canvas canvas2; + var root = new TestRoot + { + Child = stack = new StackPanel + { + Children = + { + (canvas1 = new Canvas { ZIndex = 1 }), + (canvas2 = new Canvas { ZIndex = 2 }), + } + } + }; + + var sceneBuilder = new SceneBuilder(); + var target = new DeferredRenderer( + root, + loop.Object, + sceneBuilder: sceneBuilder, + dispatcher: dispatcher); + + root.Renderer = target; + target.Start(); + RunFrame(target); + + root.InvalidateVisual(); + canvas1.ZIndex = 3; + + RunFrame(target); + + var scene = target.UnitTestScene(); + var stackNode = scene.FindNode(stack); + + Assert.Same(stackNode.Children[0].Visual, canvas2); + Assert.Same(stackNode.Children[1].Visual, canvas1); + } + [Fact] public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity() { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs index 504f0ada86..936a5d16a2 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs @@ -282,5 +282,52 @@ namespace Avalonia.Visuals.UnitTests Assert.True(called); } + + [Fact] + public void Changing_ZIndex_Should_InvalidateVisual() + { + Canvas canvas1; + var renderer = new Mock(); + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + (canvas1 = new Canvas()), + new Canvas(), + }, + }, + }; + + root.Renderer = renderer.Object; + canvas1.ZIndex = 10; + + renderer.Verify(x => x.AddDirty(canvas1)); + } + + [Fact] + public void Changing_ZIndex_Should_Recalculate_Parent_Children() + { + Canvas canvas1; + StackPanel stackPanel; + var renderer = new Mock(); + var root = new TestRoot + { + Child = stackPanel = new StackPanel + { + Children = + { + (canvas1 = new Canvas()), + new Canvas(), + }, + }, + }; + + root.Renderer = renderer.Object; + canvas1.ZIndex = 10; + + renderer.Verify(x => x.RecalculateChildren(stackPanel)); + } } }