From 32d7938c7cdbc346d3eb938c27b9d400dbceffb7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Jul 2019 00:36:55 +0200 Subject: [PATCH 01/11] Added failing tests for #2714. --- .../Rendering/DeferredRenderer.cs | 2 + .../Rendering/DeferredRendererTests.cs | 174 ++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 0d077d2a3a..b6546eee08 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -229,6 +229,8 @@ namespace Avalonia.Rendering internal void UnitTestRender() => Render(false); + internal Scene UnitTestScene() => _scene.Item; + private void Render(bool forceComposite) { using (var l = _lock.TryLock()) 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() { From bc5a101faf32f9fdb119176a8f8344aa84eefa1a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Jul 2019 17:29:53 +0200 Subject: [PATCH 02/11] Render changes to child order/zindex. Fixes #2714. --- src/Avalonia.Controls/Panel.cs | 2 +- src/Avalonia.Styling/StyledElement.cs | 44 +++++++++---------- .../Rendering/DeferredRenderer.cs | 14 +++++- src/Avalonia.Visuals/Rendering/IRenderer.cs | 6 +++ .../Rendering/ImmediateRenderer.cs | 3 ++ .../Rendering/SceneGraph/VisualNode.cs | 31 +++++++++++++ src/Avalonia.Visuals/Visual.cs | 17 +++++++ tests/Avalonia.LeakTests/ControlTests.cs | 4 ++ 8 files changed, 97 insertions(+), 24 deletions(-) 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 b6546eee08..536e0831ed 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()) @@ -518,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 4e95d21a48..98915be18d 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..89d09ae58d 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,16 @@ namespace Avalonia } } + /// + /// Called when the property changes on any control. + /// + /// The event args. + private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) + { + var parent = (e.Sender as Visual)?._visualParent; + 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) { } From 0e7f4cac81e64c81acef315795371f7f91df3cab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Jul 2019 22:29:58 +0200 Subject: [PATCH 03/11] Ensure control is invalidated on ZIndex change. --- .../Rendering/DeferredRenderer.cs | 1 - src/Avalonia.Visuals/Visual.cs | 4 +- .../Avalonia.Visuals.UnitTests/VisualTests.cs | 47 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 536e0831ed..bf1799bbdc 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -561,7 +561,6 @@ namespace Avalonia.Rendering } } - System.Diagnostics.Debug.WriteLine("Invalidated " + rect); SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); } } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 89d09ae58d..1f2d67b69e 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -514,7 +514,9 @@ namespace Avalonia /// The event args. private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e) { - var parent = (e.Sender as Visual)?._visualParent; + var sender = e.Sender as IVisual; + var parent = sender?.VisualParent; + sender?.InvalidateVisual(); parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); } 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)); + } } } From 41e9999ae76d8a1245ace66abc93fd53decb2eb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Jul 2019 15:27:31 +0200 Subject: [PATCH 04/11] Move MouseTestHelper to Avalonia.UnitTests. --- tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs | 2 +- .../Primitives/SelectingItemsControlTests_Multiple.cs | 2 +- .../Avalonia.Interactivity.UnitTests.csproj | 2 +- tests/Avalonia.Interactivity.UnitTests/GestureTests.cs | 2 +- .../MouseTestHelper.cs | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) rename tests/{Avalonia.Controls.UnitTests => Avalonia.UnitTests}/MouseTestHelper.cs (98%) 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.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 index 69bdf58f9d..a37a2450d1 100644 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using Avalonia.Controls; -using Avalonia.Controls.UnitTests; using Avalonia.Input; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Interactivity.UnitTests 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 { From 2c9114d2a2b5fac6ef046588ff4aefea319e0f56 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Jul 2019 15:37:13 +0200 Subject: [PATCH 05/11] Moved gesture tests to Avalonia.Input. As `Gestures` is defined here, not in Avalonia.Interactivity. --- .../GesturesTests.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{Avalonia.Interactivity.UnitTests/GestureTests.cs => Avalonia.Input.UnitTests/GesturesTests.cs} (99%) diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs similarity index 99% rename from tests/Avalonia.Interactivity.UnitTests/GestureTests.cs rename to tests/Avalonia.Input.UnitTests/GesturesTests.cs index a37a2450d1..fdd6487c53 100644 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -9,7 +9,7 @@ using Xunit; namespace Avalonia.Interactivity.UnitTests { - public class GestureTests + public class GesturesTests { private MouseTestHelper _mouse = new MouseTestHelper(); From 5e2b3c56e6001b7afba4bbfcc61d712773423bf7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Jul 2019 16:01:18 +0200 Subject: [PATCH 06/11] Update tests for #2730. --- .../Avalonia.Input.UnitTests/GesturesTests.cs | 137 ++++++++++++++---- 1 file changed, 108 insertions(+), 29 deletions(-) diff --git a/tests/Avalonia.Input.UnitTests/GesturesTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs index fdd6487c53..97940423a7 100644 --- a/tests/Avalonia.Input.UnitTests/GesturesTests.cs +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -23,12 +23,7 @@ namespace Avalonia.Interactivity.UnitTests }; 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")); + AddHandlers(decorator, border, result, false); _mouse.Click(border); @@ -36,7 +31,7 @@ namespace Avalonia.Interactivity.UnitTests } [Fact] - public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled() + public void Tapped_Should_Be_Raised_Even_When_Pressed_Released_Handled() { Border border = new Border(); var decorator = new Decorator @@ -45,13 +40,45 @@ namespace Avalonia.Interactivity.UnitTests }; 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")); + AddHandlers(decorator, border, result, true); _mouse.Click(border); - Assert.Equal(new[] { "bt", "dt" }, result); + 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] @@ -64,14 +91,7 @@ namespace Avalonia.Interactivity.UnitTests }; 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")); + AddHandlers(decorator, border, result, false); _mouse.Click(border); _mouse.Down(border, clickCount: 2); @@ -80,7 +100,7 @@ namespace Avalonia.Interactivity.UnitTests } [Fact] - public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled() + public void DoubleTapped_Should_Be_Raised_Even_When_Pressed_Released_Handled() { Border border = new Border(); var decorator = new Decorator @@ -89,24 +109,83 @@ namespace Avalonia.Interactivity.UnitTests }; 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"); - e.Handled = true; + + if (markHandled) + { + e.Handled = true; + } + }); + + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => + { + result.Add("dr"); + + if (markHandled) + { + 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")); + + 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")); - - _mouse.Click(border); - _mouse.Down(border, clickCount: 2); - - Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result); } } } From 6809fe11d23fe904eb43065b79d3ade16805028b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Jul 2019 16:04:57 +0200 Subject: [PATCH 07/11] Raise tap gestures even if press/release were handled. Also don't raised tapped events for right button clicks. Fixes #2730. --- src/Avalonia.Input/Gestures.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 65195394ab..fa8fb8af31 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -46,7 +46,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,7 +62,7 @@ namespace Avalonia.Input if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) { - if (!ev.Handled) + if (e.MouseButton != MouseButton.Right) { ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); } From d0a6f48015b97c9f8c86e1a43f4fb18a052aa4a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Jul 2019 16:08:16 +0200 Subject: [PATCH 08/11] Added `Gestures.RightTapped`. --- src/Avalonia.Input/Gestures.cs | 11 +++++++---- tests/Avalonia.Input.UnitTests/GesturesTests.cs | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index fa8fb8af31..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)); @@ -62,10 +67,8 @@ namespace Avalonia.Input if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) { - if (e.MouseButton != MouseButton.Right) - { - ((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/tests/Avalonia.Input.UnitTests/GesturesTests.cs b/tests/Avalonia.Input.UnitTests/GesturesTests.cs index 97940423a7..39c219a773 100644 --- a/tests/Avalonia.Input.UnitTests/GesturesTests.cs +++ b/tests/Avalonia.Input.UnitTests/GesturesTests.cs @@ -81,6 +81,23 @@ namespace Avalonia.Interactivity.UnitTests 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() { From 4b34f4770a05428f6bb9148b419bff2c1d63cf7c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jul 2019 18:41:38 +0200 Subject: [PATCH 09/11] Revert "Improve Button and ToggleButton styling" --- src/Avalonia.Themes.Default/Button.xaml | 6 +++--- src/Avalonia.Themes.Default/ToggleButton.xaml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) 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 From e82d67f66454bde076a1250009f17a2ad0fb54c9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jul 2019 19:30:07 +0200 Subject: [PATCH 10/11] Prevent NRE in VisualNode.HitTest. Not sure how this is happening but judging by #2758, it can happen. Defensively check for a null `Item` to prevent this. Fixes #2758. --- src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 4e95d21a48..12cfc7cbe3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -236,7 +236,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var operation in DrawOperations) { - if (operation.Item.HitTest(p)) + if (operation?.Item?.HitTest(p) == true) { return true; } From 142ead4d39e553ccd77dd422215bd6e6b65416a1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jul 2019 22:25:23 +0200 Subject: [PATCH 11/11] Make tests compile on CI. --- tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj | 1 + 1 file changed, 1 insertion(+) 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