Browse Source

Updated adorners and hit testing to account for RenderTransforms (#538)

Updated adorners and hit testing to account for RenderTransforms. Fixes #433.
Jeremy Koritzinsky 9 years ago
parent
commit
4fef640371

+ 9 - 3
Avalonia.sln

@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 14
-VisualStudioVersion = 14.0.24720.0
+VisualStudioVersion = 14.0.25123.0
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
 EndProject
@@ -159,19 +159,25 @@ EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4
+		src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4
+		src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
 		src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4
-		src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
 		src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
+		src\Shared\RenderHelpers\RenderHelpers.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4
 		samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4
+		samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4
-		src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
 		src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
+		src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4
+		samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4
 		samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4
+		src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4
+		src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4
 		src\Shared\PlatformSupport\PlatformSupport.projitems*{e1aa3dbf-9056-4530-9376-18119a7a3ffe}*SharedItemsImports = 4
 	EndGlobalSection

+ 3 - 0
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Specialized;
 using System.Linq;
 using Avalonia.VisualTree;
+using Avalonia.Media;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -58,6 +59,8 @@ namespace Avalonia.Controls.Primitives
 
                 if (info != null)
                 {
+                    child.RenderTransform = new MatrixTransform(info.Bounds.Transform);
+                    child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute);
                     child.Arrange(info.Bounds.Bounds);
                 }
                 else

+ 8 - 5
src/Avalonia.Input/InputExtensions.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.VisualTree;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -23,14 +24,13 @@ namespace Avalonia.Input
         public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
         {
             Contract.Requires<ArgumentNullException>(element != null);
+            var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element);
+            var geometry = transformedBounds.GetTransformedBoundsGeometry();
 
-            if (element.Bounds.Contains(p) &&
-                element.IsVisible &&
+            if (element.IsVisible &&
                 element.IsHitTestVisible &&
                 element.IsEnabledCore)
             {
-                p -= element.Bounds.Position;
-
                 if (element.VisualChildren.Any())
                 {
                     foreach (var child in ZSort(element.VisualChildren.OfType<IInputElement>()))
@@ -42,7 +42,10 @@ namespace Avalonia.Input
                     }
                 }
 
-                yield return element;
+                if (geometry.FillContains(p))
+                {
+                    yield return element;
+                }
             }
         }
 

+ 3 - 1
src/Avalonia.SceneGraph/Media/DrawingContext.cs

@@ -44,6 +44,8 @@ namespace Avalonia.Media
 
         private Matrix _currentTransform = Matrix.Identity;
 
+        private Matrix _currentContainerTransform = Matrix.Identity;
+        
         /// <summary>
         /// Gets the current transform of the drawing context.
         /// </summary>
@@ -57,7 +59,7 @@ namespace Avalonia.Media
             }
         }
 
-        private Matrix _currentContainerTransform = Matrix.Identity;
+        internal Matrix CurrentContainerTransform => _currentContainerTransform;
 
         /// <summary>
         /// Draws a bitmap image.

+ 10 - 0
src/Avalonia.SceneGraph/Media/Geometry.cs

@@ -66,5 +66,15 @@ namespace Avalonia.Media
         {
             return PlatformImpl.GetRenderBounds(strokeThickness);
         }
+
+        /// <summary>
+        /// Indicates whether the geometry contains the specified point.
+        /// </summary>
+        /// <param name="point">The point.</param>
+        /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
+        public bool FillContains(Point point)
+        {
+            return PlatformImpl.FillContains(point);
+        }
     }
 }

+ 7 - 0
src/Avalonia.SceneGraph/Platform/IGeometryImpl.cs

@@ -24,5 +24,12 @@ namespace Avalonia.Platform
         /// <param name="strokeThickness">The stroke thickness.</param>
         /// <returns>The bounding rectangle.</returns>
         Rect GetRenderBounds(double strokeThickness);
+
+        /// <summary>
+        /// Indicates whether the geometry contains the specified point.
+        /// </summary>
+        /// <param name="point">The point.</param>
+        /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
+        bool FillContains(Point point);
     }
 }

+ 6 - 0
src/Avalonia.SceneGraph/Rendering/RendererMixin.cs

@@ -122,6 +122,12 @@ namespace Avalonia.Rendering
                 using (context.PushTransformContainer())
                 {
                     visual.Render(context);
+                    var transformed =
+                        new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
+                    if (visual is Visual)
+                    {
+                        BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
+                    }
 
                     var lst = GetSortedVisualList(visual.VisualChildren);
 

+ 11 - 30
src/Avalonia.SceneGraph/VisualTree/BoundsTracker.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Linq;
+using Avalonia.Media;
 
 namespace Avalonia.VisualTree
 {
@@ -16,6 +17,9 @@ namespace Avalonia.VisualTree
     /// </remarks>
     public class BoundsTracker
     {
+        private static AttachedProperty<TransformedBounds> TransformedBoundsProperty =
+            AvaloniaProperty.RegisterAttached<BoundsTracker, Visual, TransformedBounds>("TransformedBounds");
+
         /// <summary>
         /// Starts tracking the specified visual.
         /// </summary>
@@ -23,42 +27,19 @@ namespace Avalonia.VisualTree
         /// <returns>An observable that returns the tracked bounds.</returns>
         public IObservable<TransformedBounds> Track(Visual visual)
         {
-            return Track(visual, (Visual)visual.GetVisualRoot());
+            return visual.GetObservable(TransformedBoundsProperty);
         }
 
-        /// <summary>
-        /// Starts tracking the specified visual relative to another control.
-        /// </summary>
-        /// <param name="visual">The visual.</param>
-        /// <param name="relativeTo">The control that the tracking should be relative to.</param>
-        /// <returns>An observable that returns the tracked bounds.</returns>
-        public IObservable<TransformedBounds> Track(Visual visual, Visual relativeTo)
+        internal static void SetTransformedBounds(Visual visual, TransformedBounds bounds)
         {
-            var visuals = visual.GetSelfAndVisualAncestors()
-                .TakeWhile(x => x != relativeTo)
-                .Reverse();
-            var boundsSubscriptions = new List<IObservable<Rect>>();
-
-            foreach (var v in visuals.Cast<Visual>())
-            {
-                boundsSubscriptions.Add(v.GetObservable(Visual.BoundsProperty));
-            }
-
-            var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds);
-
-            // TODO: Track transform and clip rectangle.
-            return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity));
+            visual.SetValue(TransformedBoundsProperty, bounds);
         }
 
         /// <summary>
-        /// Sums a collection of rectangles.
+        /// Gets the transformed bounds of the visual.
         /// </summary>
-        /// <param name="rects">The collection of rectangles.</param>
-        /// <returns>The summed rectangle.</returns>
-        private static Rect ExtractBounds(IList<Rect> rects)
-        {
-            var position = rects.Select(x => x.Position).Aggregate((a, b) => a + b);
-            return new Rect(position, rects.Last().Size);
-        }
+        /// <param name="visual">The visual.</param>
+        /// <returns>The transformed bounds.</returns>
+        public static TransformedBounds GetTransformedBounds(Visual visual) => visual.GetValue(TransformedBoundsProperty);
     }
 }

+ 21 - 3
src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs

@@ -1,15 +1,17 @@
 // 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.Media;
+
 namespace Avalonia.VisualTree
 {
     /// <summary>
-    /// Holds information about the bounds of a control, together with a transform and a clip/
+    /// Holds information about the bounds of a control, together with a transform and a clip.
     /// </summary>
-    public class TransformedBounds
+    public struct TransformedBounds
     {
         /// <summary>
-        /// Initializes a new instance of the <see cref="TransformedBounds"/> class.
+        /// Initializes a new instance of the <see cref="TransformedBounds"/> struct.
         /// </summary>
         /// <param name="bounds">The control's bounds.</param>
         /// <param name="clip">The control's clip rectangle.</param>
@@ -35,5 +37,21 @@ namespace Avalonia.VisualTree
         /// Gets the control's transform.
         /// </summary>
         public Matrix Transform { get; }
+
+        public Geometry GetTransformedBoundsGeometry()
+        {
+            StreamGeometry geometry = new StreamGeometry();
+            using (var context = geometry.Open())
+            {
+                context.SetFillRule(FillRule.EvenOdd);
+                context.BeginFigure(Bounds.TopLeft * Transform, true);
+                context.LineTo(Bounds.TopRight * Transform);
+                context.LineTo(Bounds.BottomRight * Transform);
+                context.LineTo(Bounds.BottomLeft * Transform);
+                context.LineTo(Bounds.TopLeft * Transform);
+                context.EndFigure(true);
+            }
+            return geometry;
+        }
     }
 }

+ 5 - 0
src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs

@@ -62,6 +62,11 @@ namespace Avalonia.Cairo.Media
             }
         }
 
+        internal bool FillContains(Point point)
+        {
+            return _context.InFill(point.X, point.Y);
+        }
+
         public void LineTo(Point point)
         {
             if (this.Path == null)

+ 5 - 0
src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs

@@ -67,5 +67,10 @@ namespace Avalonia.Cairo.Media
         {
             return _impl;
         }
+
+        public bool FillContains(Point point)
+        {
+            return _impl.FillContains(point);
+        }
     }
 }

+ 7 - 0
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@@ -75,6 +75,13 @@ namespace Avalonia.Skia
             return new StreamContext(this);
         }
 
+        public bool FillContains(Point point)
+        {
+            // TODO: Not supported by SkiaSharp yet, so use expanded Rect
+            // return EffectivePath.Contains(point.X, point.Y);
+            return GetRenderBounds(0).Contains(point);
+        }
+
         class StreamContext : IStreamGeometryContextImpl
         {
             private readonly StreamGeometryImpl _geometryImpl;

+ 3 - 0
src/Skia/Avalonia.Skia/readme.md

@@ -4,6 +4,9 @@ BitmapImpl
 - constructor from Width/Height
 - Save
 
+StreamGeometryImpl
+- Hit testing in Geometry missing as SkiaSharp does not expose this
+
 DrawingContextImpl
 - Alpha support missing as SkiaSharp does not expose this
 - Gradient Shader caching?

+ 7 - 0
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@@ -84,5 +84,12 @@ namespace Avalonia.Direct2D1.Media
                 return DefiningGeometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
             }
         }
+
+
+        public bool FillContains(Point point)
+        {
+            return Geometry.FillContainsPoint(point.ToSharpDX());
+        }
+
     }
 }

+ 5 - 1
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@@ -35,6 +35,10 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Moq, Version=4.2.1510.2205, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Xml.Linq" />

+ 270 - 46
tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs

@@ -1,9 +1,17 @@
 // 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;
 using Avalonia.Controls;
 using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
+using Moq;
 using Xunit;
+using System.Collections.Generic;
+using System.IO;
 
 namespace Avalonia.Input.UnitTests
 {
@@ -12,59 +20,73 @@ namespace Avalonia.Input.UnitTests
         [Fact]
         public void InputHitTest_Should_Find_Control_At_Point()
         {
-            var container = new Decorator
+            using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
             {
-                Width = 200,
-                Height = 200,
-                Child = new Border
+                var container = new Decorator
                 {
-                    Width = 100,
-                    Height = 100,
-                    HorizontalAlignment = HorizontalAlignment.Center,
-                    VerticalAlignment = VerticalAlignment.Center
-                }
-            };
+                    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));
+                container.Measure(Size.Infinity);
+                container.Arrange(new Rect(container.DesiredSize));
 
-            var result = container.InputHitTest(new Point(100, 100));
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
+                context.Render(container);
 
-            Assert.Equal(container.Child, result);
+                var result = container.InputHitTest(new Point(100, 100));
+
+                Assert.Equal(container.Child, result); 
+            }
         }
 
         [Fact]
         public void InputHitTest_Should_Not_Find_Control_Outside_Point()
         {
-            var container = new Decorator
+            using (UnitTestApplication.Start(new TestServices(renderInterface:new MockRenderInterface())))
             {
-                Width = 200,
-                Height = 200,
-                Child = new Border
+                var container = new Decorator
                 {
-                    Width = 100,
-                    Height = 100,
-                    HorizontalAlignment = HorizontalAlignment.Center,
-                    VerticalAlignment = VerticalAlignment.Center
-                }
-            };
+                    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));
 
-            container.Measure(Size.Infinity);
-            container.Arrange(new Rect(container.DesiredSize));
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
+                context.Render(container);
 
-            var result = container.InputHitTest(new Point(10, 10));
+                var result = container.InputHitTest(new Point(10, 10));
 
-            Assert.Equal(container, result);
+                Assert.Equal(container, result); 
+            }
         }
 
         [Fact]
         public void InputHitTest_Should_Find_Top_Control_At_Point()
         {
-            var container = new Panel
+            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
             {
-                Width = 200,
-                Height = 200,
-                Children = new Controls.Controls
+                var container = new Panel
+                {
+                    Width = 200,
+                    Height = 200,
+                    Children = new Controls.Controls
                 {
                     new Border
                     {
@@ -81,24 +103,30 @@ namespace Avalonia.Input.UnitTests
                         VerticalAlignment = VerticalAlignment.Center
                     }
                 }
-            };
+                };
+
+                container.Measure(Size.Infinity);
+                container.Arrange(new Rect(container.DesiredSize));
 
-            container.Measure(Size.Infinity);
-            container.Arrange(new Rect(container.DesiredSize));
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
+                context.Render(container);
 
-            var result = container.InputHitTest(new Point(100, 100));
+                var result = container.InputHitTest(new Point(100, 100));
 
-            Assert.Equal(container.Children[1], result);
+                Assert.Equal(container.Children[1], result); 
+            }
         }
 
         [Fact]
         public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder()
         {
-            var container = new Panel
+            using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface())))
             {
-                Width = 200,
-                Height = 200,
-                Children = new Controls.Controls
+                var container = new Panel
+                {
+                    Width = 200,
+                    Height = 200,
+                    Children = new Controls.Controls
                 {
                     new Border
                     {
@@ -116,14 +144,210 @@ namespace Avalonia.Input.UnitTests
                         VerticalAlignment = VerticalAlignment.Center
                     }
                 }
-            };
+                };
+
+                container.Measure(Size.Infinity);
+                container.Arrange(new Rect(container.DesiredSize));
+
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
+                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,
+                    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<IDrawingContextImpl>());
+                context.Render(container);
+
+                var result = container.InputHitTest(new Point(120, 120));
+
+                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 CreateRenderer(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();
+                    }
 
-            container.Measure(Size.Infinity);
-            container.Arrange(new Rect(container.DesiredSize));
+                    set
+                    {
+                        throw new NotImplementedException();
+                    }
+                }
 
-            var result = container.InputHitTest(new Point(100, 100));
+                public IStreamGeometryImpl Clone()
+                {
+                    return this;
+                }
 
-            Assert.Equal(container.Children[0], result);
+                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<Point> points = new List<Point>();
+                    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;
+                    }
+                }
+            }
         }
     }
 }

+ 1 - 0
tests/Avalonia.Input.UnitTests/packages.config

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <packages>
+  <package id="Moq" version="4.2.1510.2205" targetFramework="net45" />
   <package id="xunit" version="2.1.0" targetFramework="net45" />
   <package id="xunit.abstractions" version="2.0.0" targetFramework="net45" />
   <package id="xunit.assert" version="2.1.0" targetFramework="net45" />

+ 5 - 1
tests/Avalonia.SceneGraph.UnitTests/Avalonia.SceneGraph.UnitTests.csproj

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
   <PropertyGroup>
@@ -127,6 +127,10 @@
       <Project>{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}</Project>
       <Name>Avalonia.Styling</Name>
     </ProjectReference>
+    <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj">
+      <Project>{88060192-33D5-4932-B0F9-8BD2763E857D}</Project>
+      <Name>Avalonia.UnitTests</Name>
+    </ProjectReference>
   </ItemGroup>
   <ItemGroup>
     <None Include="app.config" />

+ 28 - 23
tests/Avalonia.SceneGraph.UnitTests/VisualTree/BoundsTrackerTests.cs

@@ -8,7 +8,11 @@ using System.Reactive.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Shapes;
 using Avalonia.VisualTree;
+using Avalonia.Rendering;
 using Xunit;
+using Avalonia.Media;
+using Moq;
+using Avalonia.UnitTests;
 
 namespace Avalonia.SceneGraph.UnitTests.VisualTree
 {
@@ -17,36 +21,37 @@ namespace Avalonia.SceneGraph.UnitTests.VisualTree
         [Fact]
         public void Should_Track_Bounds()
         {
-            var target = new BoundsTracker();
-            var control = default(Rectangle);
-            var tree = new Decorator
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                Padding = new Thickness(10),
-                Child = new Decorator
+                var target = new BoundsTracker();
+                var control = default(Rectangle);
+                var tree = new Decorator
                 {
-                    Padding = new Thickness(5),
-                    Child = control = new Rectangle
+                    Padding = new Thickness(10),
+                    Child = new Decorator
                     {
-                        Width = 15,
-                        Height = 15,
-                    },
-                }
-            };
+                        Padding = new Thickness(5),
+                        Child = control = new Rectangle
+                        {
+                            Width = 15,
+                            Height = 15,
+                        },
+                    }
+                };
 
-            tree.Measure(Size.Infinity);
-            tree.Arrange(new Rect(0, 0, 100, 100));
+                var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
 
-            var track = target.Track(control, tree);
-            var results = new List<TransformedBounds>();
-            track.Subscribe(results.Add);
+                tree.Measure(Size.Infinity);
+                tree.Arrange(new Rect(0, 0, 100, 100));
+                context.Render(tree);
 
-            Assert.Equal(new Rect(42, 42, 15, 15), results[0].Bounds);
+                var track = target.Track(control);
+                var results = new List<TransformedBounds>();
+                track.Subscribe(results.Add);
 
-            tree.Padding = new Thickness(15);
-            tree.Measure(Size.Infinity);
-            tree.Arrange(new Rect(0, 0, 100, 100));
-
-            Assert.Equal(new Rect(37, 37, 15, 15), results[1].Bounds);
+                Assert.Equal(new Rect(0, 0, 15, 15), results[0].Bounds);
+                Assert.Equal(Matrix.CreateTranslation(42, 42), results[0].Transform);
+            }
         }
     }
 }