From c1dd44ead4cf42d13bb36b4ef74b1caa52a4c35e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 21 Oct 2018 23:48:43 +0300 Subject: [PATCH 1/7] unit test for issue #2000 button with render transform don't trigger click --- .../ButtonTests.cs | 165 +++++++++++++++++- 1 file changed, 163 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index d218960726..9c9d09d4f8 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -1,7 +1,10 @@ using System; using System.Windows.Input; using Avalonia.Data; -using Avalonia.Markup.Data; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.VisualTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -92,6 +95,164 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsEnabled); } + [Fact] + public void Button_Is_Raising_Click() + { + var mouse = Mock.Of(); + IInputElement captured = null; + Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50)); + Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); + Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + + var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100) }; + + bool clicked = false; + + target.Click += (s, e) => clicked = true; + + RaisePointerEnter(target, mouse); + RaisePointerMove(target, mouse); + RaisePointerPressed(target, mouse, 1, MouseButton.Left); + + Assert.Equal(captured, target); + + RaisePointerReleased(target, mouse, MouseButton.Left); + + Assert.Equal(captured, null); + + Assert.True(clicked); + } + + [Fact] + public void Button_Is_Not_Raising_Click_When_PointerReleased_Outside() + { + var mouse = Mock.Of(); + IInputElement captured = null; + Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50)); + Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); + Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + + var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100) }; + + bool clicked = false; + + target.Click += (s, e) => clicked = true; + + RaisePointerEnter(target, mouse); + RaisePointerMove(target, mouse); + RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerLeave(target, mouse); + + Assert.Equal(captured, target); + + RaisePointerReleased(target, mouse, MouseButton.Left); + + Assert.Equal(captured, null); + + Assert.False(clicked); + } + + [Fact] + public void Button_With_RenderTransform_Is_Raising_Click() + { + var mouse = Mock.Of(); + IInputElement captured = null; + Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50)); + Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); + Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + + var target = new TestButton() + { + Bounds = new Rect(0, 0, 100, 100), + RenderTransform = new TranslateTransform { X = 100, Y = 0 } + }; + + //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels + //so mouse with x=150 coordinates should trigger click + //button shouldn't count on bounds to calculate pointer is in the over or not, but + //on avalonia event system, as renderer hit test will properly calculate whether to send + //mouse over events to button based on rendered bounds + //note: button also may have not rectangular shape and only renderer hit testing is reliable + + bool clicked = false; + + target.Click += (s, e) => clicked = true; + + RaisePointerEnter(target, mouse); + RaisePointerMove(target, mouse); + RaisePointerPressed(target, mouse, 1, MouseButton.Left); + + Assert.Equal(captured, target); + + RaisePointerReleased(target, mouse, MouseButton.Left); + + Assert.Equal(captured, null); + + Assert.True(clicked); + } + + private class TestButton : Button + { + public new Rect Bounds + { + get => base.Bounds; + set => base.Bounds = value; + } + } + + private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton) + { + button.RaiseEvent(new PointerPressedEventArgs + { + RoutedEvent = InputElement.PointerPressedEvent, + Source = button, + MouseButton = mouseButton, + ClickCount = clickCount, + Device = device, + }); + } + + private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton) + { + button.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + Source = button, + MouseButton = mouseButton, + Device = device, + }); + } + + private void RaisePointerEnter(Button button, IMouseDevice device) + { + button.RaiseEvent(new PointerEventArgs + { + RoutedEvent = InputElement.PointerEnterEvent, + Source = button, + Device = device, + }); + } + + private void RaisePointerLeave(Button button, IMouseDevice device) + { + button.RaiseEvent(new PointerEventArgs + { + RoutedEvent = InputElement.PointerLeaveEvent, + Source = button, + Device = device, + }); + } + + private void RaisePointerMove(Button button, IMouseDevice device) + { + button.RaiseEvent(new PointerEventArgs + { + RoutedEvent = InputElement.PointerMovedEvent, + Source = button, + Device = device, + }); + } + private class TestCommand : ICommand { private bool _enabled; @@ -123,4 +284,4 @@ namespace Avalonia.Controls.UnitTests } } } -} \ No newline at end of file +} From e1874b4a4c442f0cda18ffe9e02d1304d783913b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 23 Oct 2018 01:13:04 +0300 Subject: [PATCH 2/7] fix for issue #2000 button with rendertransform don't fire click --- src/Avalonia.Controls/Button.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 24b2af7996..9c4c33f549 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -251,7 +251,9 @@ namespace Avalonia.Controls IsPressed = false; e.Handled = true; - if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this))) + //only renderer (hittesting) know better whether pointer is over the bounds of the button + if (ClickMode == ClickMode.Release && + (IsPointerOver || new Rect(Bounds.Size).Contains(e.GetPosition(this)))) { OnClick(); } From 922917b29937ca0cc07e4a2d7d62c3bc5e0f8503 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 23 Oct 2018 14:12:00 +0300 Subject: [PATCH 3/7] fix pr nits --- src/Avalonia.Controls/Button.cs | 1 - tests/Avalonia.Controls.UnitTests/ButtonTests.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 9c4c33f549..0e8a765e57 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -251,7 +251,6 @@ namespace Avalonia.Controls IsPressed = false; e.Handled = true; - //only renderer (hittesting) know better whether pointer is over the bounds of the button if (ClickMode == ClickMode.Release && (IsPointerOver || new Rect(Bounds.Size).Contains(e.GetPosition(this)))) { diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9c9d09d4f8..afc53f2fa9 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -96,7 +96,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Button_Is_Raising_Click() + public void Button_Raises_Click() { var mouse = Mock.Of(); IInputElement captured = null; @@ -124,7 +124,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Button_Is_Not_Raising_Click_When_PointerReleased_Outside() + public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() { var mouse = Mock.Of(); IInputElement captured = null; @@ -153,7 +153,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Button_With_RenderTransform_Is_Raising_Click() + public void Button_With_RenderTransform_Raises_Click() { var mouse = Mock.Of(); IInputElement captured = null; From ef158e3ec36bdd32bec03a9aa36b8ca88b48f7be Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 26 Oct 2018 10:59:49 +0300 Subject: [PATCH 4/7] hittest when button is pressed --- src/Avalonia.Controls/Button.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 0e8a765e57..dc65df2584 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using System.Windows.Input; using Avalonia.Data; using Avalonia.Input; @@ -251,8 +252,9 @@ namespace Avalonia.Controls IsPressed = false; e.Handled = true; - if (ClickMode == ClickMode.Release && - (IsPointerOver || new Rect(Bounds.Size).Contains(e.GetPosition(this)))) + var hittest = VisualRoot?.Renderer?.HitTest(e.GetPosition(VisualRoot), VisualRoot, null); + + if (ClickMode == ClickMode.Release && hittest?.Any(v => v == this) == true) { OnClick(); } @@ -262,9 +264,9 @@ namespace Avalonia.Controls protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) { base.UpdateDataValidation(property, status); - if(property == CommandProperty) + if (property == CommandProperty) { - if(status?.ErrorType == BindingErrorType.Error) + if (status?.ErrorType == BindingErrorType.Error) { IsEnabled = false; } From 87437b82327b63bfffa58c6a860747d07c045313 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 26 Oct 2018 11:29:00 +0300 Subject: [PATCH 5/7] update button unit tests --- .../ButtonTests.cs | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index afc53f2fa9..c318229700 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -3,6 +3,8 @@ using System.Windows.Input; using Avalonia.Data; using Avalonia.Input; using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.VisualTree; using Moq; using Xunit; @@ -99,12 +101,20 @@ namespace Avalonia.Controls.UnitTests public void Button_Raises_Click() { var mouse = Mock.Of(); + var renderer = Mock.Of(); IInputElement captured = null; Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50)); Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), null)) + .Returns>((p, r, f) => + r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); - var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100) }; + var target = new TestButton() + { + Bounds = new Rect(0, 0, 100, 100), + Renderer = renderer + }; bool clicked = false; @@ -127,12 +137,20 @@ namespace Avalonia.Controls.UnitTests public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() { var mouse = Mock.Of(); + var renderer = Mock.Of(); IInputElement captured = null; Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50)); Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), null)) + .Returns>((p, r, f) => + r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); - var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100) }; + var target = new TestButton() + { + Bounds = new Rect(0, 0, 100, 100), + Renderer = renderer + }; bool clicked = false; @@ -156,15 +174,21 @@ namespace Avalonia.Controls.UnitTests public void Button_With_RenderTransform_Raises_Click() { var mouse = Mock.Of(); + var renderer = Mock.Of(); IInputElement captured = null; Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50)); Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), null)) + .Returns>((p, r, f) => + r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? + new IVisual[] { r } : new IVisual[0]); var target = new TestButton() { Bounds = new Rect(0, 0, 100, 100), - RenderTransform = new TranslateTransform { X = 100, Y = 0 } + RenderTransform = new TranslateTransform { X = 100, Y = 0 }, + Renderer = renderer }; //actual bounds of button should be 100,0,100,100 x -> translated 100 pixels @@ -191,13 +215,27 @@ namespace Avalonia.Controls.UnitTests Assert.True(clicked); } - private class TestButton : Button + private class TestButton : Button, IRenderRoot { public new Rect Bounds { get => base.Bounds; set => base.Bounds = value; } + + public Size ClientSize => throw new NotImplementedException(); + + public IRenderer Renderer { get; set; } + + public double RenderScaling => throw new NotImplementedException(); + + public IRenderTarget CreateRenderTarget() => throw new NotImplementedException(); + + public void Invalidate(Rect rect) => throw new NotImplementedException(); + + public Point PointToClient(Point point) => throw new NotImplementedException(); + + public Point PointToScreen(Point point) => throw new NotImplementedException(); } private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton) From 08d552de108c613d3f0bd745ff48ba80aa89a1aa Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 26 Oct 2018 16:11:38 +0300 Subject: [PATCH 6/7] fix button click for deferedrenderer --- src/Avalonia.Controls/Button.cs | 6 ++++-- tests/Avalonia.Controls.UnitTests/ButtonTests.cs | 11 ++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index dc65df2584..2d80af8e4a 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -7,6 +7,7 @@ using System.Windows.Input; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -252,9 +253,10 @@ namespace Avalonia.Controls IsPressed = false; e.Handled = true; - var hittest = VisualRoot?.Renderer?.HitTest(e.GetPosition(VisualRoot), VisualRoot, null); + var hittest = this.GetVisualsAt(e.GetPosition(this)); - if (ClickMode == ClickMode.Release && hittest?.Any(v => v == this) == true) + if (ClickMode == ClickMode.Release && + hittest.Any(c => c == this || (c as IStyledElement)?.TemplatedParent == this) == true) { OnClick(); } diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index c318229700..76f2898700 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50)); Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); - Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), null)) + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50)); Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); - Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), null)) + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -179,7 +179,7 @@ namespace Avalonia.Controls.UnitTests Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50)); Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); - Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), null)) + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? new IVisual[] { r } : new IVisual[0]); @@ -217,6 +217,11 @@ namespace Avalonia.Controls.UnitTests private class TestButton : Button, IRenderRoot { + public TestButton() + { + IsVisible = true; + } + public new Rect Bounds { get => base.Bounds; From 81c274328b96ba319711ff15c537086aa97011ce Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 2 Nov 2018 11:04:29 +0200 Subject: [PATCH 7/7] fix tiny tiny nit --- src/Avalonia.Controls/Button.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 2d80af8e4a..1f3fcbafb3 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -256,7 +256,7 @@ namespace Avalonia.Controls var hittest = this.GetVisualsAt(e.GetPosition(this)); if (ClickMode == ClickMode.Release && - hittest.Any(c => c == this || (c as IStyledElement)?.TemplatedParent == this) == true) + hittest.Any(c => c == this || (c as IStyledElement)?.TemplatedParent == this)) { OnClick(); }