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.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 65195394ab..02dda45e99 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -18,6 +18,11 @@ namespace Avalonia.Input RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register( + "RightTapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureEvent = RoutedEvent.Register( "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); @@ -46,7 +51,7 @@ namespace Avalonia.Input } else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) { - if (!ev.Handled) + if (e.MouseButton != MouseButton.Right) { e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); } @@ -62,10 +67,8 @@ namespace Avalonia.Input if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) { - if (!ev.Handled) - { - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); - } + var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; + ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et)); } } } 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.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 6ed1f6d8fc..698ddec2a8 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -22,13 +22,13 @@ - - - + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml index 41f366fdf9..9e05c38eef 100644 --- a/src/Avalonia.Themes.Default/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/ToggleButton.xaml @@ -22,17 +22,17 @@ - - - - + \ No newline at end of file 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.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 2a61ff1566..27ddd95d20 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -9,8 +9,8 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; -using Avalonia.Markup.Data; using Avalonia.Styling; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 4bcfeb6d03..be0f4272a5 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -13,7 +13,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Markup.Data; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives diff --git a/tests/Avalonia.Input.UnitTests/GesturesTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs new file mode 100644 index 0000000000..39c219a773 --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -0,0 +1,208 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Interactivity.UnitTests +{ + public class GesturesTests + { + private MouseTestHelper _mouse = new MouseTestHelper(); + + [Fact] + public void Tapped_Should_Follow_Pointer_Pressed_Released() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, false); + + _mouse.Click(border); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_Even_When_Pressed_Released_Handled() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, true); + + _mouse.Click(border); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_For_Middle_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Middle); + + Assert.True(raised); + } + + [Fact] + public void Tapped_Should_Not_Be_Raised_For_Right_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.TappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Right); + + Assert.False(raised); + } + + [Fact] + public void RightTapped_Should_Be_Raised_For_Right_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.RightTappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Right); + + Assert.True(raised); + } + + [Fact] + public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, false); + + _mouse.Click(border); + _mouse.Down(border, clickCount: 2); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Be_Raised_Even_When_Pressed_Released_Handled() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + AddHandlers(decorator, border, result, true); + + _mouse.Click(border); + _mouse.Down(border, clickCount: 2); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Be_Raised_For_Middle_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Middle); + _mouse.Down(border, MouseButton.Middle, clickCount: 2); + + Assert.True(raised); + } + + [Fact] + public void DoubleTapped_Should_Not_Be_Raised_For_Right_Button() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var raised = false; + + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => raised = true); + + _mouse.Click(border, MouseButton.Right); + _mouse.Down(border, MouseButton.Right, clickCount: 2); + + Assert.False(raised); + } + + private void AddHandlers( + Decorator decorator, + Border border, + IList result, + bool markHandled) + { + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => + { + result.Add("dp"); + + if (markHandled) + { + e.Handled = true; + } + }); + + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => + { + result.Add("dr"); + + if (markHandled) + { + e.Handled = true; + } + }); + + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + } + } +} diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 7316b1de3d..2bde78ad63 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -20,7 +20,7 @@ - + diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs deleted file mode 100644 index 69bdf58f9d..0000000000 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Controls.UnitTests; -using Avalonia.Input; -using Xunit; - -namespace Avalonia.Interactivity.UnitTests -{ - public class GestureTests - { - private MouseTestHelper _mouse = new MouseTestHelper(); - - [Fact] - public void Tapped_Should_Follow_Pointer_Pressed_Released() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - - _mouse.Click(border); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); - } - - [Fact] - public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - border.AddHandler(Border.PointerPressedEvent, (s, e) => e.Handled = true); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - - _mouse.Click(border); - - Assert.Equal(new[] { "bt", "dt" }, result); - } - - [Fact] - public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); - - _mouse.Click(border); - _mouse.Down(border, clickCount: 2); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); - } - - [Fact] - public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled() - { - Border border = new Border(); - var decorator = new Decorator - { - Child = border - }; - var result = new List(); - - decorator.AddHandler(Border.PointerPressedEvent, (s, e) => - { - result.Add("dp"); - e.Handled = true; - }); - - decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); - decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); - decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); - border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); - border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); - border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); - - _mouse.Click(border); - _mouse.Down(border, clickCount: 2); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result); - } - } -} 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.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index f065fcb63d..ae901ca2f2 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -1,6 +1,7 @@  netstandard2.0 + latest false Library false diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs similarity index 98% rename from tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs rename to tests/Avalonia.UnitTests/MouseTestHelper.cs index 373bbaed75..00ad850cf8 100644 --- a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -1,9 +1,8 @@ -using System.Reactive; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; -namespace Avalonia.Controls.UnitTests +namespace Avalonia.UnitTests { public class MouseTestHelper { 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)); + } } }