From d91d1829ac00a7a77e132bbe5f5905dbf25eade5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Nov 2016 18:34:26 +0100 Subject: [PATCH] Initial implementation of scenegraph hit testing. Based solely on control bounds as before. --- .../RenderTest/RenderTest.v2.ncrunchproject | 7 +- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + .../Rendering/DeferredRenderer.cs | 12 + src/Avalonia.Visuals/Rendering/IRenderer.cs | 2 + src/Avalonia.Visuals/Rendering/Renderer.cs | 6 + .../Rendering/SceneGraph/GeometryNode.cs | 5 + .../Rendering/SceneGraph/ISceneNode.cs | 3 +- .../Rendering/SceneGraph/IVisualNode.cs | 4 +- .../Rendering/SceneGraph/ImageNode.cs | 5 + .../Rendering/SceneGraph/LineNode.cs | 5 + .../Rendering/SceneGraph/RectangleNode.cs | 5 + .../Rendering/SceneGraph/Scene.cs | 48 +- .../Rendering/SceneGraph/SceneBuilder.cs | 14 +- .../Rendering/SceneGraph/TextNode.cs | 5 + .../Rendering/SceneGraph/VisualNode.cs | 9 +- .../Rendering/ZIndexComparer.cs | 13 + .../VisualTree/VisualExtensions.cs | 28 +- .../Avalonia.Input.UnitTests.csproj | 1 - .../InputElement_HitTesting.cs | 484 ------------------ tests/Avalonia.LeakTests/ControlTests.cs | 14 - ...alonia.Cairo.RenderTests.v2.ncrunchproject | 8 +- tests/Avalonia.UnitTests/TestRoot.cs | 10 +- tests/Avalonia.UnitTests/TestServices.cs | 11 +- .../Avalonia.UnitTests/UnitTestApplication.cs | 17 +- ...alonia.Visuals.UnitTests.v2.ncrunchproject | 7 +- .../Rendering/SceneGraph/SceneBuilderTests.cs | 66 +++ .../VisualExtensionsTests_GetVisualsAt.cs | 250 +++++---- 27 files changed, 371 insertions(+), 669 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/ZIndexComparer.cs delete mode 100644 tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs diff --git a/samples/RenderTest/RenderTest.v2.ncrunchproject b/samples/RenderTest/RenderTest.v2.ncrunchproject index 30815b1937..8e69ee9b4a 100644 --- a/samples/RenderTest/RenderTest.v2.ncrunchproject +++ b/samples/RenderTest/RenderTest.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + MissingOrIgnoredProjectReference \ No newline at end of file diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index aeffde81ed..69caba1738 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -124,6 +124,7 @@ + diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 3721544234..d9fd8e5012 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -6,6 +6,7 @@ using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.VisualTree; +using System.Collections.Generic; namespace Avalonia.Rendering { @@ -30,6 +31,7 @@ namespace Avalonia.Rendering _root = root; _scene = new Scene(root); + _needsUpdate = true; _renderLoop = renderLoop; _renderLoop.Tick += OnRenderLoopTick; } @@ -50,6 +52,16 @@ namespace Avalonia.Rendering _renderLoop.Tick -= OnRenderLoopTick; } + public IEnumerable HitTest(Point p, Func filter) + { + if (_needsUpdate) + { + UpdateScene(); + } + + return _scene.HitTest(p, filter); + } + public void Render(Rect rect) { if (_renderTarget == null) diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 119bb4c8d1..a297e82203 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -3,6 +3,7 @@ using System; using Avalonia.VisualTree; +using System.Collections.Generic; namespace Avalonia.Rendering { @@ -11,6 +12,7 @@ namespace Avalonia.Rendering bool DrawFps { get; set; } void AddDirty(IVisual visual); + IEnumerable HitTest(Point p, Func filter); void Render(Rect rect); } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/Renderer.cs b/src/Avalonia.Visuals/Rendering/Renderer.cs index 3b2fe19be4..504fd40396 100644 --- a/src/Avalonia.Visuals/Rendering/Renderer.cs +++ b/src/Avalonia.Visuals/Rendering/Renderer.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Platform; using Avalonia.VisualTree; +using System.Collections.Generic; namespace Avalonia.Rendering { @@ -35,6 +36,11 @@ namespace Avalonia.Rendering _renderLoop.Tick -= OnRenderLoopTick; } + public IEnumerable HitTest(Point p, Func filter) + { + throw new NotImplementedException(); + } + public void Render(Rect rect) { if (_renderTarget == null) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 89850f0c0e..db05533d4a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -35,5 +35,10 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs index 073d9716d7..e022970c1e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using Avalonia.Media; namespace Avalonia.Rendering.SceneGraph { public interface ISceneNode { + bool HitTest(Point p); + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 17fcb7e219..ddaf3a4e9c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -9,7 +9,9 @@ namespace Avalonia.Rendering.SceneGraph { public interface IVisualNode : ISceneNode { - IReadOnlyList Children { get; } IVisual Visual { get; } + Rect ClipBounds { get; set; } + bool ClipToBounds { get; set; } + IReadOnlyList Children { get; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index f8aef2aed9..6d3e5c6a56 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -38,5 +38,10 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = Transform; context.DrawImage(Source, Opacity, SourceRect, DestRect); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 9ff0fbde4e..4b30e94b7d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -31,5 +31,10 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = Transform; context.DrawLine(Pen, P1, P2); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 738cd189ad..d4fed636a1 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -46,5 +46,10 @@ namespace Avalonia.Rendering.SceneGraph context.DrawRectangle(Pen, Rect, CornerRadius); } } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index f0212d23c3..77c1a45c90 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -31,6 +31,14 @@ namespace Avalonia.Rendering.SceneGraph _index.Add(node.Visual, node); } + public Scene Clone() + { + var index = new Dictionary(); + var root = (VisualNode)Clone((VisualNode)Root, null, index); + var result = new Scene(root, index); + return result; + } + public IVisualNode FindNode(IVisual visual) { IVisualNode node; @@ -38,12 +46,9 @@ namespace Avalonia.Rendering.SceneGraph return node; } - public Scene Clone() + public IEnumerable HitTest(Point p, Func filter) { - var index = new Dictionary(); - var root = (VisualNode)Clone((VisualNode)Root, null, index); - var result = new Scene(root, index); - return result; + return HitTest(Root, p, null, filter); } private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary index) @@ -68,5 +73,38 @@ namespace Avalonia.Rendering.SceneGraph return result; } + + private IEnumerable HitTest(IVisualNode node, Point p, Rect? clip, Func filter) + { + if (filter?.Invoke(node.Visual) != false) + { + if (node.ClipToBounds) + { + // TODO: Handle geometry clip. + clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); + } + + if (!clip.HasValue || clip.Value.Contains(p)) + { + for (var i = node.Children.Count - 1; i >= 0; --i) + { + var visualChild = node.Children[i] as IVisualNode; + + if (visualChild != null) + { + foreach (var h in HitTest(visualChild, p, clip, filter)) + { + yield return h; + } + } + } + + if (node.HitTest(p)) + { + yield return node.Visual; + } + } + } + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index b3ac55fe6a..4749c99925 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.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 Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; @@ -55,7 +56,7 @@ namespace Avalonia.Rendering.SceneGraph using (context.PushTransformContainer()) { node.Transform = contextImpl.Transform; - node.Bounds = bounds; + node.ClipBounds = bounds * node.Transform; node.ClipToBounds = clipToBounds; node.GeometryClip = visual.Clip; node.Opacity = opacity; @@ -63,16 +64,7 @@ namespace Avalonia.Rendering.SceneGraph visual.Render(context); -#pragma warning disable 0618 - var transformed = new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); -#pragma warning restore 0618 - - if (visual is Visual) - { - BoundsTracker.SetTransformedBounds((Visual)visual, transformed); - } - - foreach (var child in visual.VisualChildren) + foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) { Update(context, scene, child, node); } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index 3718f16af4..f9bc16c807 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -35,5 +35,10 @@ namespace Avalonia.Rendering.SceneGraph origin == Origin && Equals(text, Text); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 3f4cea5c30..1482d7d468 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph public IVisual Visual { get; } public Matrix Transform { get; set; } - public Rect Bounds { get; set; } + public Rect ClipBounds { get; set; } public bool ClipToBounds { get; set; } public Geometry GeometryClip { get; set; } public double Opacity { get; set; } @@ -32,6 +32,11 @@ namespace Avalonia.Rendering.SceneGraph return new VisualNode(Visual); } + public bool HitTest(Point p) + { + return ClipBounds.Contains(p); + } + public void Render(IDrawingContextImpl context) { context.Transform = Transform; @@ -43,7 +48,7 @@ namespace Avalonia.Rendering.SceneGraph if (ClipToBounds) { - context.PushClip(Bounds); + context.PushClip(ClipBounds); } foreach (var child in Children) diff --git a/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs b/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs new file mode 100644 index 0000000000..b9c43bcbc3 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering +{ + public class ZIndexComparer : IComparer + { + public static readonly ZIndexComparer Instance = new ZIndexComparer(); + + public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex); + } +} diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index e7876b762f..6eaef3363d 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -1,6 +1,7 @@ // 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 Avalonia.Rendering; using System; using System.Collections.Generic; using System.Linq; @@ -102,26 +103,9 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - if (filter?.Invoke(visual) != false) - { - bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)visual)?.Contains(p) == true; - - if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Any()) - { - foreach (var child in visual.VisualChildren.SortByZIndex()) - { - foreach (var result in child.GetVisualsAt(p, filter)) - { - yield return result; - } - } - } - - if (containsPoint) - { - yield return visual; - } - } + var root = visual.GetVisualRoot(); + p = visual.TranslatePoint(p, root); + return root.Renderer.HitTest(p, filter); } /// @@ -197,11 +181,11 @@ namespace Avalonia.VisualTree /// /// The root visual or null if the visual is not rooted. /// - public static IVisual GetVisualRoot(this IVisual visual) + public static IRenderRoot GetVisualRoot(this IVisual visual) { Contract.Requires(visual != null); - return visual.VisualRoot as IVisual; + return visual as IRenderRoot ?? visual.VisualRoot; } /// diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index 5ed05e9e33..26c73ad00a 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -86,7 +86,6 @@ - diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs deleted file mode 100644 index e4e7551f5c..0000000000 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ /dev/null @@ -1,484 +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 Avalonia.Controls; -using Avalonia.Controls.Presenters; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.UnitTests; -using Moq; -using System; -using System.Collections.Generic; -using System.IO; -using Xunit; - -namespace Avalonia.Input.UnitTests -{ - public class InputElement_HitTesting - { - [Fact] - public void InputHitTest_Should_Find_Control_At_Point() - { - using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Decorator - { - Width = 200, - Height = 200, - Child = new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(100, 100)); - - Assert.Equal(container.Child, result); - } - } - - [Fact] - public void InputHitTest_Should_Not_Find_Control_Outside_Point() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Decorator - { - Width = 200, - Height = 200, - Child = new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(10, 10)); - - Assert.Equal(container, result); - } - } - - [Fact] - public void InputHitTest_Should_Find_Top_Control_At_Point() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Panel - { - Width = 200, - Height = 200, - Children = new Controls.Controls - { - new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(100, 100)); - - Assert.Equal(container.Children[1], result); - } - } - - [Fact] - public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Panel - { - Width = 200, - Height = 200, - Children = new Controls.Controls - { - new Border - { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(100, 100)); - - Assert.Equal(container.Children[0], result); - } - } - - [Fact] - public void InputHitTest_Should_Find_Control_Translated_Outside_Parent_Bounds() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - Border target; - var container = new Panel - { - Width = 200, - Height = 200, - ClipToBounds = false, - Children = new Controls.Controls - { - new Border - { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - Child = target = new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - RenderTransform = new TranslateTransform(110, 110), - } - }, - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(120, 120)); - - Assert.Equal(target, result); - } - } - - [Fact] - public void InputHitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - Border target; - - var container = new Panel - { - Width = 100, - Height = 200, - Children = new Controls.Controls - { - new Panel() - { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - ClipToBounds = true, - Children = new Controls.Controls - { - (target = new Border() - { - Width = 100, - Height = 100, - Margin = new Thickness(0, -100, 0, 0) - }) - } - } - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(50, 50)); - - Assert.NotEqual(target, result); - Assert.Equal(container, result); - } - } - - [Fact] - public void InputHitTest_Should_Not_Find_Control_Outside_Scroll_ViewPort() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - Border target; - Border item1; - Border item2; - ScrollContentPresenter scroll; - - var container = new Panel - { - Width = 100, - Height = 200, - Children = new Controls.Controls - { - (target = new Border() - { - Width = 100, - Height = 100 - }), - new Border() - { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - Child = scroll = new ScrollContentPresenter() - { - Content = new StackPanel() - { - Children = new Controls.Controls - { - (item1 = new Border() - { - Width = 100, - Height = 100, - }), - (item2 = new Border() - { - Width = 100, - Height = 100, - }), - } - } - } - } - } - }; - - scroll.UpdateChild(); - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(50, 150)); - - Assert.Equal(item1, result); - - result = container.InputHitTest(new Point(50, 50)); - - Assert.Equal(target, result); - - scroll.Offset = new Vector(0, 100); - - //we don't have setup LayoutManager so we will make it manually - scroll.Parent.InvalidateArrange(); - container.InvalidateArrange(); - - container.Arrange(new Rect(container.DesiredSize)); - context.Render(container); - - result = container.InputHitTest(new Point(50, 150)); - - Assert.Equal(item2, result); - - result = container.InputHitTest(new Point(50, 50)); - - Assert.NotEqual(item1, result); - Assert.Equal(target, result); - } - } - - class MockRenderInterface : IPlatformRenderInterface - { - public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) - { - throw new NotImplementedException(); - } - - public IRenderTarget CreateRenderTarget(IPlatformHandle handle) - { - throw new NotImplementedException(); - } - - public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height) - { - throw new NotImplementedException(); - } - - public IStreamGeometryImpl CreateStreamGeometry() - { - return new MockStreamGeometry(); - } - - public IBitmapImpl LoadBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(string fileName) - { - throw new NotImplementedException(); - } - - class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl - { - private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); - public Rect Bounds - { - get - { - throw new NotImplementedException(); - } - } - - public Matrix Transform - { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } - } - - public IStreamGeometryImpl Clone() - { - return this; - } - - public bool FillContains(Point point) - { - return _impl.FillContains(point); - } - - public Rect GetRenderBounds(double strokeThickness) - { - throw new NotImplementedException(); - } - - public IStreamGeometryContextImpl Open() - { - return _impl; - } - - class MockStreamGeometryContext : IStreamGeometryContextImpl - { - private List points = new List(); - public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) - { - throw new NotImplementedException(); - } - - public void BeginFigure(Point startPoint, bool isFilled) - { - points.Add(startPoint); - } - - public void CubicBezierTo(Point point1, Point point2, Point point3) - { - throw new NotImplementedException(); - } - - public void Dispose() - { - } - - public void EndFigure(bool isClosed) - { - } - - public void LineTo(Point point) - { - points.Add(point); - } - - public void QuadraticBezierTo(Point control, Point endPoint) - { - throw new NotImplementedException(); - } - - public void SetFillRule(FillRule fillRule) - { - } - - public bool FillContains(Point point) - { - // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html - // to determine if the point is in the geometry (since it will always be convex in this situation) - for (int i = 0; i < points.Count; i++) - { - var a = points[i]; - var b = points[(i + 1) % points.Count]; - var c = points[(i + 2) % points.Count]; - - Vector v0 = c - a; - Vector v1 = b - a; - Vector v2 = point - a; - - var dot00 = v0 * v0; - var dot01 = v0 * v1; - var dot02 = v0 * v2; - var dot11 = v1 * v1; - var dot12 = v1 * v2; - - - var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); - var u = (dot11 * dot02 - dot01 * dot12) * invDenom; - var v = (dot00 * dot12 - dot01 * dot02) * invDenom; - if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; - } - return false; - } - } - } - } - } -} diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 1e257a698c..3f9b260225 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -54,7 +54,6 @@ namespace Avalonia.LeakTests }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -90,7 +89,6 @@ namespace Avalonia.LeakTests }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -127,7 +125,6 @@ namespace Avalonia.LeakTests }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -163,7 +160,6 @@ namespace Avalonia.LeakTests }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -207,7 +203,6 @@ namespace Avalonia.LeakTests }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -294,21 +289,12 @@ namespace Avalonia.LeakTests }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } - private static void PurgeMoqReferences() - { - // Moq holds onto references in its mock of IRenderer in case we want to check if a method has been called; - // clear these. - var renderer = Mock.Get(AvaloniaLocator.Current.GetService()); - renderer.ResetCalls(); - } - private class Node { public string Name { get; set; } diff --git a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject index ed3fd080b1..7c25da7d15 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject @@ -7,7 +7,7 @@ true false false - false + true false false true @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 064d5d36d3..8d20ed66e6 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -15,6 +15,13 @@ namespace Avalonia.UnitTests { private readonly NameScope _nameScope = new NameScope(); + public TestRoot() + { + var rendererFactory = AvaloniaLocator.Current.GetService(); + var renderLoop = AvaloniaLocator.Current.GetService(); + Renderer = rendererFactory?.CreateRenderer(this, renderLoop); + } + event EventHandler INameScope.Registered { add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; } @@ -41,7 +48,7 @@ namespace Avalonia.UnitTests public IRenderTarget RenderTarget => null; - public IRenderer Renderer => null; + public IRenderer Renderer { get; } public IRenderTarget CreateRenderTarget() { @@ -50,7 +57,6 @@ namespace Avalonia.UnitTests public void Invalidate(Rect rect) { - throw new NotImplementedException(); } public Point PointToClient(Point p) => p; diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 8dc838163f..7e0437c7f3 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -21,7 +21,7 @@ namespace Avalonia.UnitTests assetLoader: new AssetLoader(), layoutManager: new LayoutManager(), platform: new AppBuilder().RuntimePlatform, - renderer: Mock.Of(), + renderer: (_, __) => Mock.Of(), renderInterface: CreateRenderInterfaceMock(), renderLoop: Mock.Of(), standardCursorFactory: Mock.Of(), @@ -42,6 +42,9 @@ namespace Avalonia.UnitTests public static readonly TestServices MockThreadingInterface = new TestServices( threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true)); + public static readonly TestServices RealDeferredRenderer = new TestServices( + renderer: (root, loop) => new DeferredRenderer(root, loop)); + public static readonly TestServices RealFocus = new TestServices( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), @@ -60,7 +63,7 @@ namespace Avalonia.UnitTests Func keyboardDevice = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, - IRenderer renderer = null, + Func renderer = null, IPlatformRenderInterface renderInterface = null, IRenderLoop renderLoop = null, IStandardCursorFactory standardCursorFactory = null, @@ -93,7 +96,7 @@ namespace Avalonia.UnitTests public Func KeyboardDevice { get; } public ILayoutManager LayoutManager { get; } public IRuntimePlatform Platform { get; } - public IRenderer Renderer { get; } + public Func Renderer { get; } public IPlatformRenderInterface RenderInterface { get; } public IRenderLoop RenderLoop { get; } public IStandardCursorFactory StandardCursorFactory { get; } @@ -110,7 +113,7 @@ namespace Avalonia.UnitTests Func keyboardDevice = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, - IRenderer renderer = null, + Func renderer = null, IPlatformRenderInterface renderInterface = null, IRenderLoop renderLoop = null, IStandardCursorFactory standardCursorFactory = null, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 0dec218432..ab71aa95cc 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -43,7 +43,7 @@ namespace Avalonia.UnitTests .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) .Bind().ToConstant(Services.LayoutManager) .Bind().ToConstant(Services.Platform) - .Bind().ToConstant(Services.Renderer) + .Bind().ToConstant(new RendererFactory(Services.Renderer)) .Bind().ToConstant(Services.RenderInterface) .Bind().ToConstant(Services.RenderLoop) .Bind().ToConstant(Services.ThreadingInterface) @@ -58,5 +58,20 @@ namespace Avalonia.UnitTests Styles.AddRange(styles); } } + + private class RendererFactory : IRendererFactory + { + Func _func; + + public RendererFactory(Func func) + { + _func = func; + } + + public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) + { + return _func?.Invoke(root, renderLoop); + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject index 30815b1937..621f626959 100644 --- a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject +++ b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + AbnormalReferenceResolution;LongTestTimesWithoutParallelExecution \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 1ccd8655bc..36ff61ed6d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Rendering.SceneGraph; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph @@ -52,6 +54,70 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Should_Respect_ZIndex() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border front; + Border back; + var tree = new TestRoot + { + Child = new Panel + { + Children = + { + (front = new Border + { + ZIndex = 1, + }), + (back = new Border + { + ZIndex = 0, + }), + } + } + }; + + var result = SceneBuilder.Update(new Scene(tree)); + + var panelNode = result.FindNode(tree.Child); + var expected = new IVisual[] { back, front }; + var actual = panelNode.Children.OfType().Select(x => x.Visual).ToArray(); + Assert.Equal(expected, actual); + } + } + + [Fact] + public void ClipBounds_Should_Be_In_Global_Coordinates() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border target; + var tree = new TestRoot + { + Child = new Decorator + { + Margin = new Thickness(24, 26), + Child = target = new Border + { + Margin = new Thickness(26, 24), + Width = 100, + Height = 100, + } + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var result = SceneBuilder.Update(new Scene(tree)); + var targetNode = result.FindNode(target); + + Assert.Equal(new Rect(50, 50, 100, 100), targetNode.ClipBounds); + } + } + [Fact] public void Should_Update_Border_Background_Node() { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs index 28ca7cdd41..39f9010755 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs @@ -11,6 +11,7 @@ using Avalonia.UnitTests; using Avalonia.VisualTree; using Moq; using Xunit; +using System; namespace Avalonia.Visuals.UnitTests.VisualTree { @@ -19,9 +20,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Find_Controls_At_Point() { - using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Decorator + var container = new TestRoot { Width = 200, Height = 200, @@ -49,9 +50,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point() { - using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Decorator + var container = new TestRoot { Width = 200, Height = 200, @@ -85,9 +86,9 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Not_Find_Control_Outside_Point() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Decorator + var container = new TestRoot { Width = 200, Height = 200, @@ -115,27 +116,31 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Return_Top_Controls_First() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 200, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border + Width = 200, + Height = 200, + Children = new Controls.Controls { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center + new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } } } }; @@ -155,36 +160,40 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 200, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - new Border + Width = 200, + Height = 200, + Children = new Controls.Controls { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 75, - Height = 75, - ZIndex = 2, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center + new Border + { + Width = 100, + Height = 100, + ZIndex = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 75, + Height = 75, + ZIndex = 2, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } } } }; @@ -204,32 +213,36 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { Border target; - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 200, - Height = 200, - ClipToBounds = false, - Children = new Controls.Controls + Child = container = new Panel { - new Border + Width = 200, + Height = 200, + ClipToBounds = false, + Children = new Controls.Controls { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - Child = target = new Border + new Border { - Width = 50, - Height = 50, + Width = 100, + Height = 100, + ZIndex = 1, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, - RenderTransform = new TranslateTransform(110, 110), - } - }, + Child = target = new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + RenderTransform = new TranslateTransform(110, 110), + } + }, + } } }; @@ -248,30 +261,33 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { Border target; - - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 100, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - new Panel() + Width = 100, + Height = 200, + Children = new Controls.Controls { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - ClipToBounds = true, - Children = new Controls.Controls + new Panel() { - (target = new Border() + Width = 100, + Height = 100, + Margin = new Thickness(0, 100, 0, 0), + ClipToBounds = true, + Children = new Controls.Controls { - Width = 100, - Height = 100, - Margin = new Thickness(0, -100, 0, 0) - }) + (target = new Border() + { + Width = 100, + Height = 100, + Margin = new Thickness(0, -100, 0, 0) + }) + } } } } @@ -292,45 +308,48 @@ namespace Avalonia.Visuals.UnitTests.VisualTree [Fact] public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { Border target; Border item1; Border item2; ScrollContentPresenter scroll; - - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 100, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - (target = new Border() - { - Width = 100, - Height = 100 - }), - new Border() + Width = 100, + Height = 200, + Children = new Controls.Controls { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - Child = scroll = new ScrollContentPresenter() + (target = new Border() { - Content = new StackPanel() + Width = 100, + Height = 100 + }), + new Border() + { + Width = 100, + Height = 100, + Margin = new Thickness(0, 100, 0, 0), + Child = scroll = new ScrollContentPresenter() { - Children = new Controls.Controls + Content = new StackPanel() { - (item1 = new Border() - { - Width = 100, - Height = 100, - }), - (item2 = new Border() + Children = new Controls.Controls { - Width = 100, - Height = 100, - }), + (item1 = new Border() + { + Width = 100, + Height = 100, + }), + (item2 = new Border() + { + Width = 100, + Height = 100, + }), + } } } } @@ -373,5 +392,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree Assert.Equal(target, result); } } + + private IDisposable TestApplication() + { + return UnitTestApplication.Start( + new TestServices( + renderInterface: new MockRenderInterface(), + renderLoop: Mock.Of(), + renderer: (root, loop) => new DeferredRenderer(root, loop))); + } } }