浏览代码

Merge remote-tracking branch 'origin/master' into scenegraph

 Conflicts:
	src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
	src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
Steven Kirk 8 年之前
父节点
当前提交
1e2e3b340b
共有 91 个文件被更改,包括 1135 次插入338 次删除
  1. 2 1
      .gitignore
  2. 13 12
      build.cake
  3. 1 1
      build/Moq.props
  4. 4 2
      build/XUnit.props
  5. 1 0
      samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
  6. 1 0
      samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
  7. 0 1
      src/Android/Avalonia.Android/AndroidPlatform.cs
  8. 2 0
      src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
  9. 1 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  10. 3 0
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  11. 1 1
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  12. 33 90
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  13. 7 0
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  14. 2 2
      src/Avalonia.Controls/Primitives/Popup.cs
  15. 1 2
      src/Avalonia.Controls/ToolTip.cs
  16. 3 0
      src/Avalonia.Controls/TopLevel.cs
  17. 0 1
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  18. 3 3
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  19. 1 2
      src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs
  20. 43 31
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  21. 2 2
      src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs
  22. 4 7
      src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs
  23. 5 5
      src/Avalonia.Diagnostics/ViewModels/TreeNode.cs
  24. 14 10
      src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
  25. 32 0
      src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs
  26. 3 4
      src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs
  27. 10 10
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  28. 30 0
      src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs
  29. 4 1
      src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs
  30. 2 1
      src/Avalonia.HtmlRenderer/HtmlControl.cs
  31. 7 0
      src/Avalonia.Input/IInputDevice.cs
  32. 8 0
      src/Avalonia.Input/IInputRoot.cs
  33. 2 1
      src/Avalonia.Input/IKeyboardDevice.cs
  34. 1 0
      src/Avalonia.Input/InputManager.cs
  35. 3 9
      src/Avalonia.Input/KeyboardDevice.cs
  36. 8 23
      src/Avalonia.Input/MouseDevice.cs
  37. 64 33
      src/Avalonia.Layout/LayoutManager.cs
  38. 12 4
      src/Avalonia.Layout/Layoutable.cs
  39. 1 1
      src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs
  40. 11 2
      src/Avalonia.Visuals/Vector.cs
  41. 0 1
      src/Gtk/Avalonia.Gtk/GtkPlatform.cs
  42. 1 1
      src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs
  43. 3 0
      src/Gtk/Avalonia.Gtk/TopLevelImpl.cs
  44. 0 1
      src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs
  45. 2 3
      src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs
  46. 1 0
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  47. 1 0
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  48. 3 3
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs
  49. 0 1
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  50. 2 2
      src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
  51. 1 1
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  52. 1 1
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  53. 2 0
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  54. 10 5
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  55. 50 0
      src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs
  56. 15 0
      src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs
  57. 6 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  58. 1 1
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs
  59. 1 1
      src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
  60. 0 1
      src/Windows/Avalonia.Win32/Win32Platform.cs
  61. 3 3
      src/Windows/Avalonia.Win32/WindowFramebuffer.cs
  62. 2 0
      src/Windows/Avalonia.Win32/WindowImpl.cs
  63. 2 2
      src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs
  64. 2 0
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  65. 0 1
      src/iOS/Avalonia.iOS/iOSPlatform.cs
  66. 1 0
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  67. 134 0
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs
  68. 1 1
      tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs
  69. 2 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  70. 65 0
      tests/Avalonia.Benchmarks/Layout/Measure.cs
  71. 1 0
      tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
  72. 1 0
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  73. 1 0
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  74. 1 0
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  75. 1 0
      tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj
  76. 265 7
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  77. 29 0
      tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs
  78. 43 0
      tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs
  79. 90 0
      tests/Avalonia.Layout.UnitTests/LayoutableTests.cs
  80. 0 24
      tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs
  81. 8 0
      tests/Avalonia.LeakTests/ControlTests.cs
  82. 1 0
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  83. 24 6
      tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
  84. 1 1
      tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs
  85. 1 0
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  86. 1 1
      tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs
  87. 1 1
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  88. 1 0
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  89. 1 3
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  90. 3 1
      tests/Avalonia.UnitTests/TestRoot.cs
  91. 4 0
      tools/packages.config

+ 2 - 1
.gitignore

@@ -162,7 +162,8 @@ $RECYCLE.BIN/
 #################
 ## Cake
 #################
-tools/
+tools/*
+!tools/packages.config
 .nuget
 artifacts/
 nuget

+ 13 - 12
build.cake

@@ -11,7 +11,7 @@
 // TOOLS
 ///////////////////////////////////////////////////////////////////////////////
 
-#tool "nuget:?package=xunit.runner.console&version=2.1.0"
+#tool "nuget:?package=xunit.runner.console&version=2.2.0"
 #tool "nuget:?package=OpenCover"
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -98,7 +98,6 @@ Task("Clean")
     CleanDirectory(parameters.TestsRoot);
 });
 
-
 Task("Restore-NuGet-Packages")
     .IsDependentOn("Clean")
     .WithCriteria(parameters.IsRunningOnWindows)
@@ -171,23 +170,25 @@ void RunCoreTest(string dir, Parameters parameters, bool net461Only)
             continue;
         Information("Running for " + fw);
         DotNetCoreTest(System.IO.Path.Combine(dir, System.IO.Path.GetFileName(dir)+".csproj"),
-            new DotNetCoreTestSettings{Framework = fw});
+            new DotNetCoreTestSettings {
+                Configuration = parameters.Configuration,
+                Framework = fw
+            });
     }
 }
 
-
 Task("Run-Net-Core-Unit-Tests")
     .IsDependentOn("Clean")
     .Does(() => {
         RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false);
-        RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, true);
-        RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, true);
-        RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, true);
-        RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, true);
-        //RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, true);
-        //RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, true);
-        RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, true);
-        RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, true);
+        RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Input.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Interactivity.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Layout.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Markup.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false);
+        RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false);
     });
 
 Task("Run-Unit-Tests")

+ 1 - 1
build/Moq.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="Moq" Version="4.7.1" />
+    <PackageReference Include="Moq" Version="4.7.25" />
   </ItemGroup>
 </Project>

+ 4 - 2
build/XUnit.props

@@ -7,7 +7,9 @@
     <PackageReference Include="xunit.extensibility.core" Version="2.2.0" />
     <PackageReference Include="xunit.extensibility.execution" Version="2.2.0" />
     <PackageReference Include="xunit.runner.console" Version="2.2.0" />
-    <PackageReference Condition="'$(TargetFramework)' == 'net461'" Include="xunit.runner.visualstudio" Version="2.2.0" />
-    <PackageReference Condition="'$(TargetFramework)' == 'netcoreapp1.1'" Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
+  </ItemGroup>
+  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.1'">
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
   </ItemGroup>
 </Project>

+ 1 - 0
samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj

@@ -22,6 +22,7 @@
     <ItemGroup>
         <ProjectReference Include="..\..\..\src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />
         <ProjectReference Include="..\..\..\src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj" />
+        <ProjectReference Include="..\..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
         <ProjectReference Include="..\..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
         <ProjectReference Include="..\..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
         <ProjectReference Include="..\..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj" />

+ 1 - 0
samples/interop/WindowsInteropTest/WindowsInteropTest.csproj

@@ -181,5 +181,6 @@
   </ItemGroup>
   <Import Project="..\..\..\build\Rx.props" />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <Import Project="..\..\..\build\SkiaSharp.props" />
   <Import Project="$(MSBuildThisFileDirectory)..\..\..\src\Shared\nuget.workaround.targets" />
 </Project>

+ 0 - 1
src/Android/Avalonia.Android/AndroidPlatform.cs

@@ -51,7 +51,6 @@ namespace Avalonia.Android
                 .Bind<IClipboard>().ToTransient<ClipboardImpl>()
                 .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
-                .Bind<IMouseDevice>().ToSingleton<AndroidMouseDevice>()
                 .Bind<IPlatformSettings>().ToConstant(Instance)
                 .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())

+ 2 - 0
src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs

@@ -4,6 +4,8 @@ namespace Avalonia.Android.Platform.Input
 {
     public class AndroidMouseDevice : MouseDevice
     {
+        public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice();
+
         public AndroidMouseDevice()
         {
 

+ 1 - 1
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         public int Width { get; }
         public int Height { get; }
         public int RowBytes { get; }
-        public Size Dpi { get; } = new Size(96, 96);
+        public Vector Dpi { get; } = new Vector(96, 96);
         public PixelFormat Format { get; }
 
         [DllImport("android")]

+ 3 - 0
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -10,6 +10,7 @@ using Avalonia.Platform;
 using System;
 using System.Collections.Generic;
 using System.Reactive.Disposables;
+using Avalonia.Android.Platform.Input;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform.Surfaces;
 
@@ -65,6 +66,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             }
         }
 
+        public IMouseDevice MouseDevice => AndroidMouseDevice.Instance;
+
         public Action Closed { get; set; }
 
         public Action<RawInputEventArgs> Input { get; set; }

+ 1 - 1
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@@ -71,7 +71,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
                 if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y)
                 {
                     var inputRoot = _getInputRoot();
-                    var mouseDevice = MouseDevice.Instance;
+                    var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance;
 
                     //in order the controls to work in a predictable way
                     //we need to generate mouse move before first mouse down event

+ 33 - 90
src/Avalonia.Base/Collections/AvaloniaListExtensions.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.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
@@ -59,7 +60,8 @@ namespace Avalonia.Collections
         /// the index in the collection and the item.
         /// </param>
         /// <param name="reset">
-        /// An action called when the collection is reset.
+        /// An action called when the collection is reset. This will be followed by calls to 
+        /// <paramref name="added"/> for each item present in the collection after the reset.
         /// </param>
         /// <returns>A disposable used to terminate the subscription.</returns>
         public static IDisposable ForEachItem<T>(
@@ -68,112 +70,38 @@ namespace Avalonia.Collections
             Action<int, T> removed,
             Action reset)
         {
-            int index;
-
-            NotifyCollectionChangedEventHandler handler = (_, e) =>
+            void Add(int index, IList items)
             {
-                switch (e.Action)
+                foreach (T item in items)
                 {
-                    case NotifyCollectionChangedAction.Add:
-                        index = e.NewStartingIndex;
-
-                        foreach (T item in e.NewItems)
-                        {
-                            added(index++, item);
-                        }
-
-                        break;
-
-                    case NotifyCollectionChangedAction.Replace:
-                        index = e.OldStartingIndex;
-
-                        foreach (T item in e.OldItems)
-                        {
-                            removed(index++, item);
-                        }
-
-                        index = e.NewStartingIndex;
-
-                        foreach (T item in e.NewItems)
-                        {
-                            added(index++, item);
-                        }
-
-                        break;
-
-                    case NotifyCollectionChangedAction.Remove:
-                        index = e.OldStartingIndex;
-
-                        foreach (T item in e.OldItems)
-                        {
-                            removed(index++, item);
-                        }
-
-                        break;
-
-                    case NotifyCollectionChangedAction.Reset:
-                        if (reset == null)
-                        {
-                            throw new InvalidOperationException(
-                                "Reset called on collection without reset handler.");
-                        }
-
-                        reset();
-                        break;
+                    added(index++, item);
                 }
-            };
+            }
 
-            index = 0;
-            foreach (T i in collection)
+            void Remove(int index, IList items)
             {
-                added(index++, i);
+                for (var i = items.Count - 1; i >= 0; --i)
+                {
+                    removed(index + i, (T)items[i]);
+                }
             }
 
-            collection.CollectionChanged += handler;
-
-            return Disposable.Create(() => collection.CollectionChanged -= handler);
-        }
-
-        /// <summary>
-        /// Invokes an action for each item in a collection and subsequently each item added or
-        /// removed from the collection.
-        /// </summary>
-        /// <typeparam name="T">The type of the collection items.</typeparam>
-        /// <param name="collection">The collection.</param>
-        /// <param name="added">
-        /// An action called initially with all items in the collection and subsequently with a
-        /// list of items added to the collection. The parameters passed are the index of the
-        /// first item added to the collection and the items added.
-        /// </param>
-        /// <param name="removed">
-        /// An action called with all items removed from the collection. The parameters passed 
-        /// are the index of the first item removed from the collection and the items removed.
-        /// </param>
-        /// <param name="reset">
-        /// An action called when the collection is reset.
-        /// </param>
-        /// <returns>A disposable used to terminate the subscription.</returns>
-        public static IDisposable ForEachItem<T>(
-            this IAvaloniaReadOnlyList<T> collection,
-            Action<int, IEnumerable<T>> added,
-            Action<int, IEnumerable<T>> removed,
-            Action reset)
-        {
             NotifyCollectionChangedEventHandler handler = (_, e) =>
             {
                 switch (e.Action)
                 {
                     case NotifyCollectionChangedAction.Add:
-                        added(e.NewStartingIndex, e.NewItems.Cast<T>());
+                        Add(e.NewStartingIndex, e.NewItems);
                         break;
 
+                    case NotifyCollectionChangedAction.Move:
                     case NotifyCollectionChangedAction.Replace:
-                        removed(e.OldStartingIndex, e.OldItems.Cast<T>());
-                        added(e.NewStartingIndex, e.NewItems.Cast<T>());
+                        Remove(e.OldStartingIndex, e.OldItems);
+                        Add(e.NewStartingIndex, e.NewItems);
                         break;
 
                     case NotifyCollectionChangedAction.Remove:
-                        removed(e.OldStartingIndex, e.OldItems.Cast<T>());
+                        Remove(e.OldStartingIndex, e.OldItems);
                         break;
 
                     case NotifyCollectionChangedAction.Reset:
@@ -184,16 +112,31 @@ namespace Avalonia.Collections
                         }
 
                         reset();
+                        Add(0, (IList)collection);
                         break;
                 }
             };
 
-            added(0, collection);
+            Add(0, (IList)collection);
             collection.CollectionChanged += handler;
 
             return Disposable.Create(() => collection.CollectionChanged -= handler);
         }
 
+        public static IAvaloniaReadOnlyList<TDerived> CreateDerivedList<TSource, TDerived>(
+            this IAvaloniaReadOnlyList<TSource> collection,
+            Func<TSource, TDerived> select)
+        {
+            var result = new AvaloniaList<TDerived>();
+
+            collection.ForEachItem(
+                (i, item) => result.Insert(i, select(item)),
+                (i, item) => result.RemoveAt(i),
+                () => result.Clear());
+
+            return result;
+        }
+
         /// <summary>
         /// Listens for property changed events from all items in a collection.
         /// </summary>

+ 7 - 0
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using JetBrains.Annotations;
 
 namespace Avalonia.Platform
 {
@@ -93,5 +94,11 @@ namespace Avalonia.Platform
         /// Gets or sets a method called when the underlying implementation is destroyed.
         /// </summary>
         Action Closed { get; set; }
+
+        /// <summary>
+        /// Gets a mouse device associated with toplevel
+        /// </summary>
+        [CanBeNull]
+        IMouseDevice MouseDevice { get; }
     }
 }

+ 2 - 2
src/Avalonia.Controls/Primitives/Popup.cs

@@ -340,11 +340,11 @@ namespace Avalonia.Controls.Primitives
             switch (mode)
             {
                 case PlacementMode.Pointer:
-                    if (MouseDevice.Instance != null)
+                    if(PopupRoot != null)
                     {
                         // Scales the Horizontal and Vertical offset to screen co-ordinates.
                         var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling);
-                        return MouseDevice.Instance.Position + screenOffset;
+                        return (((IInputRoot)PopupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset;
                     }
 
                     return default(Point);

+ 1 - 2
src/Avalonia.Controls/ToolTip.cs

@@ -109,8 +109,7 @@ namespace Avalonia.Controls
                 {
                     throw new AvaloniaInternalException("Previous ToolTip not disposed.");
                 }
-
-                var cp = MouseDevice.Instance?.GetPosition(control);
+                var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control);
                 var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22);
 
                 s_popup = new PopupRoot();

+ 3 - 0
src/Avalonia.Controls/TopLevel.cs

@@ -163,6 +163,9 @@ namespace Avalonia.Controls
             set { SetValue(PointerOverElementProperty, value); }
         }
 
+        /// <inheritdoc/>
+        IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
+
         /// <summary>
         /// Gets or sets a value indicating whether access keys are shown in the window.
         /// </summary>

+ 0 - 1
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@@ -34,7 +34,6 @@
     <ProjectReference Include="..\Avalonia.Input\Avalonia.Input.csproj" />
     <ProjectReference Include="..\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
     <ProjectReference Include="..\Avalonia.Layout\Avalonia.Layout.csproj" />
-    <ProjectReference Include="..\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
     <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />

+ 3 - 3
src/Avalonia.Diagnostics/DevTools.xaml.cs

@@ -11,7 +11,6 @@ using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia
 {
@@ -74,7 +73,7 @@ namespace Avalonia.Diagnostics
                         Content = devTools,
                         DataTemplates = new DataTemplates
                         {
-                            new ViewLocator<ReactiveObject>(),
+                            new ViewLocator<ViewModelBase>(),
                         }
                     };
 
@@ -107,7 +106,8 @@ namespace Avalonia.Diagnostics
 
             if ((e.Modifiers) == modifiers)
             {
-                var point = MouseDevice.Instance.GetPosition(Root);
+
+                var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point);
                 var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
                     .FirstOrDefault();
 

+ 1 - 2
src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -4,11 +4,10 @@
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class ControlDetailsViewModel : ReactiveObject
+    internal class ControlDetailsViewModel : ViewModelBase
     {
         public ControlDetailsViewModel(IVisual control)
         {

+ 43 - 31
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -5,32 +5,50 @@ using System;
 using System.Reactive.Linq;
 using Avalonia.Controls;
 using Avalonia.Input;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class DevToolsViewModel : ReactiveObject
+    internal class DevToolsViewModel : ViewModelBase
     {
-        private ReactiveObject _content;
-
+        private ViewModelBase _content;
         private int _selectedTab;
-
         private TreePageViewModel _logicalTree;
-
         private TreePageViewModel _visualTree;
-
-        private readonly ObservableAsPropertyHelper<string> _focusedControl;
-
-        private readonly ObservableAsPropertyHelper<string> _pointerOverElement;
+        private string _focusedControl;
+        private string _pointerOverElement;
 
         public DevToolsViewModel(IControl root)
         {
             _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
             _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
 
-            this.WhenAnyValue(x => x.SelectedTab).Subscribe(index =>
+            UpdateFocusedControl();
+            KeyboardDevice.Instance.PropertyChanged += (s, e) =>
             {
-                switch (index)
+                if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
+                {
+                    UpdateFocusedControl();
+                }
+            };
+
+            root.GetObservable(TopLevel.PointerOverElementProperty)
+                .Subscribe(x => PointerOverElement = x?.GetType().Name);
+        }
+
+        public ViewModelBase Content
+        {
+            get { return _content; }
+            private set { RaiseAndSetIfChanged(ref _content, value); }
+        }
+
+        public int SelectedTab
+        {
+            get { return _selectedTab; }
+            set
+            {
+                _selectedTab = value;
+
+                switch (value)
                 {
                     case 0:
                         Content = _logicalTree;
@@ -39,34 +57,23 @@ namespace Avalonia.Diagnostics.ViewModels
                         Content = _visualTree;
                         break;
                 }
-            });
 
-            _focusedControl = KeyboardDevice.Instance
-                .WhenAnyValue(x => x.FocusedElement)
-                .Select(x => x?.GetType().Name)
-                .ToProperty(this, x => x.FocusedControl);
-
-            _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty)
-                .Select(x => x?.GetType().Name)
-                .ToProperty(this, x => x.PointerOverElement);
+                RaisePropertyChanged();
+            }
         }
 
-        public ReactiveObject Content
+        public string FocusedControl
         {
-            get { return _content; }
-            private set { this.RaiseAndSetIfChanged(ref _content, value); }
+            get { return _focusedControl; }
+            private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
         }
 
-        public int SelectedTab
+        public string PointerOverElement
         {
-            get { return _selectedTab; }
-            set { this.RaiseAndSetIfChanged(ref _selectedTab, value); }
+            get { return _pointerOverElement; }
+            private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
         }
 
-        public string FocusedControl => _focusedControl.Value;
-
-        public string PointerOverElement => _pointerOverElement.Value;
-
         public void SelectControl(IControl control)
         {
             var tree = Content as TreePageViewModel;
@@ -76,5 +83,10 @@ namespace Avalonia.Diagnostics.ViewModels
                 tree.SelectControl(control);
             }
         }
+
+        private void UpdateFocusedControl()
+        {
+            _focusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
+        }
     }
 }

+ 2 - 2
src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs

@@ -2,9 +2,9 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -13,7 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public LogicalTreeNode(ILogical logical, TreeNode parent)
             : base((Control)logical, parent)
         {
-            Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x, this));
+            Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this));
         }
 
         public static LogicalTreeNode[] Create(object control)

+ 4 - 7
src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs

@@ -3,16 +3,13 @@
 
 using System;
 using Avalonia.Data;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class PropertyDetails : ReactiveObject
+    internal class PropertyDetails : ViewModelBase
     {
         private object _value;
-
         private string _priority;
-
         private string _diagnostic;
 
         public PropertyDetails(AvaloniaObject o, AvaloniaProperty property)
@@ -41,19 +38,19 @@ namespace Avalonia.Diagnostics.ViewModels
         public string Priority
         {
             get { return _priority; }
-            private set { this.RaiseAndSetIfChanged(ref _priority, value); }
+            private set { RaiseAndSetIfChanged(ref _priority, value); }
         }
 
         public string Diagnostic
         {
             get { return _diagnostic; }
-            private set { this.RaiseAndSetIfChanged(ref _diagnostic, value); }
+            private set { RaiseAndSetIfChanged(ref _diagnostic, value); }
         }
 
         public object Value
         {
             get { return _value; }
-            private set { this.RaiseAndSetIfChanged(ref _value, value); }
+            private set { RaiseAndSetIfChanged(ref _value, value); }
         }
     }
 }

+ 5 - 5
src/Avalonia.Diagnostics/ViewModels/TreeNode.cs

@@ -5,13 +5,13 @@ using System;
 using System.Collections.Specialized;
 using System.Reactive;
 using System.Reactive.Linq;
+using Avalonia.Collections;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreeNode : ReactiveObject
+    internal class TreeNode : ViewModelBase
     {
         private string _classes;
         private bool _isExpanded;
@@ -47,7 +47,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
         }
 
-        public IReadOnlyReactiveList<TreeNode> Children
+        public IAvaloniaReadOnlyList<TreeNode> Children
         {
             get;
             protected set;
@@ -56,7 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public string Classes
         {
             get { return _classes; }
-            private set { this.RaiseAndSetIfChanged(ref _classes, value); }
+            private set { RaiseAndSetIfChanged(ref _classes, value); }
         }
 
         public IVisual Visual
@@ -67,7 +67,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public bool IsExpanded
         {
             get { return _isExpanded; }
-            set { this.RaiseAndSetIfChanged(ref _isExpanded, value); }
+            set { RaiseAndSetIfChanged(ref _isExpanded, value); }
         }
 
         public TreeNode Parent

+ 14 - 10
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs

@@ -1,25 +1,19 @@
 // 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.Reactive.Linq;
 using Avalonia.Controls;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
-    internal class TreePageViewModel : ReactiveObject
+    internal class TreePageViewModel : ViewModelBase
     {
         private TreeNode _selected;
-
-        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
+        private ControlDetailsViewModel _details;
 
         public TreePageViewModel(TreeNode[] nodes)
         {
             Nodes = nodes;
-            _details = this.WhenAnyValue(x => x.SelectedNode)
-                .Select(x => x != null ? new ControlDetailsViewModel(x.Visual) : null)
-                .ToProperty(this, x => x.Details);
         }
 
         public TreeNode[] Nodes { get; protected set; }
@@ -27,10 +21,20 @@ namespace Avalonia.Diagnostics.ViewModels
         public TreeNode SelectedNode
         {
             get { return _selected; }
-            set { this.RaiseAndSetIfChanged(ref _selected, value); }
+            set
+            {
+                if (RaiseAndSetIfChanged(ref _selected, value))
+                {
+                    Details = value != null ? new ControlDetailsViewModel(value.Visual) : null;
+                }
+            }
         }
 
-        public ControlDetailsViewModel Details => _details.Value;
+        public ControlDetailsViewModel Details
+        {
+            get { return _details; }
+            private set { RaiseAndSetIfChanged(ref _details, value); }
+        }
 
         public TreeNode FindNode(IControl control)
         {

+ 32 - 0
src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using JetBrains.Annotations;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    public class ViewModelBase : INotifyPropertyChanged
+    {
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        [NotifyPropertyChangedInvocator]
+        protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+        {
+            if (!EqualityComparer<T>.Default.Equals(field, value))
+            {
+                field = value;
+                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+                return true;
+            }
+
+            return false;
+        }
+
+        [NotifyPropertyChangedInvocator]
+        protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+    }
+}

+ 3 - 4
src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs

@@ -1,10 +1,9 @@
 // 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.Collections;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -17,11 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels
 
             if (host?.Root == null)
             {
-                Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this));
+                Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this));
             }
             else
             {
-                Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
+                Children = new AvaloniaList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
             }
 
             if ((Visual is IStyleable styleable))

+ 10 - 10
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@@ -8,7 +8,6 @@ using Avalonia.Controls;
 using Avalonia.Diagnostics.ViewModels;
 using Avalonia.Media;
 using Avalonia.Styling;
-using ReactiveUI;
 
 namespace Avalonia.Diagnostics.Views
 {
@@ -16,6 +15,7 @@ namespace Avalonia.Diagnostics.Views
     {
         private static readonly StyledProperty<ControlDetailsViewModel> ViewModelProperty =
             AvaloniaProperty.Register<ControlDetailsView, ControlDetailsViewModel>("ViewModel");
+        private SimpleGrid _grid;
 
         public ControlDetailsView()
         {
@@ -27,7 +27,11 @@ namespace Avalonia.Diagnostics.Views
         public ControlDetailsViewModel ViewModel
         {
             get { return GetValue(ViewModelProperty); }
-            private set { SetValue(ViewModelProperty, value); }
+            private set
+            {
+                SetValue(ViewModelProperty, value);
+                _grid[GridRepeater.ItemsProperty] = value?.Properties;
+            }
         }
 
         private void InitializeComponent()
@@ -36,7 +40,7 @@ namespace Avalonia.Diagnostics.Views
 
             Content = new ScrollViewer
             {
-                Content = new SimpleGrid
+                Content = _grid = new SimpleGrid
                 {
                     Styles = new Styles
                     {
@@ -49,7 +53,6 @@ namespace Avalonia.Diagnostics.Views
                         },
                     },
                     [GridRepeater.TemplateProperty] = pt,
-                    [!GridRepeater.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Properties).ToBinding(),
                 }
             };
         }
@@ -62,16 +65,13 @@ namespace Avalonia.Diagnostics.Views
             {
                 Text = property.Name,
                 TextWrapping = TextWrapping.NoWrap,
-                [!ToolTip.TipProperty] = property
-                    .WhenAnyValue(x => x.Diagnostic)
-                    .ToBinding(),
+                [!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding(),
             };
 
             yield return new TextBlock
             {
                 TextWrapping = TextWrapping.NoWrap,
-                [!TextBlock.TextProperty] = property
-                    .WhenAnyValue(v => v.Value)
+                [!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
                     .Select(v => v?.ToString())
                     .ToBinding(),
             };
@@ -79,7 +79,7 @@ namespace Avalonia.Diagnostics.Views
             yield return new TextBlock
             {
                 TextWrapping = TextWrapping.NoWrap,
-                [!TextBlock.TextProperty] = property.WhenAnyValue(x => x.Priority).ToBinding(),
+                [!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding(),
             };
         }
     }

+ 30 - 0
src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs

@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Linq;
+using System.Reflection;
+
+namespace Avalonia.Diagnostics.Views
+{
+    internal static class PropertyChangedExtenions
+    {
+        public static IObservable<T> GetObservable<T>(this INotifyPropertyChanged source, string propertyName)
+        {
+            Contract.Requires<ArgumentNullException>(source != null);
+            Contract.Requires<ArgumentNullException>(propertyName != null);
+
+            var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName);
+
+            if (property == null)
+            {
+                throw new ArgumentException($"Property '{propertyName}' not found on '{source}.");
+            }
+
+            return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
+                e => source.PropertyChanged += e,
+                e => source.PropertyChanged -= e)
+                    .Where(e => e.EventArgs.PropertyName == propertyName)
+                    .Select(_ => (T)property.GetValue(source))
+                    .StartWith((T)property.GetValue(source));
+        }
+    }
+}

+ 4 - 1
src/Avalonia.HtmlRenderer/Adapters/ControlAdapter.cs

@@ -10,9 +10,11 @@
 // - Sun Tsu,
 // "The Art of War"
 
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Html;
 using Avalonia.Input;
+using Avalonia.VisualTree;
 using TheArtOfDev.HtmlRenderer.Adapters;
 using TheArtOfDev.HtmlRenderer.Adapters.Entities;
 using TheArtOfDev.HtmlRenderer.Core.Utils;
@@ -54,7 +56,8 @@ namespace TheArtOfDev.HtmlRenderer.Avalonia.Adapters
         {
             get
             {
-                return Util.Convert(MouseDevice.Instance.GetPosition(_control));
+                var pos = (_control.GetVisualRoot() as IInputRoot)?.MouseDevice?.Position ?? default(Point);
+                return Util.Convert(pos);
             }
         }
 

+ 2 - 1
src/Avalonia.HtmlRenderer/HtmlControl.cs

@@ -17,6 +17,7 @@ using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
 using TheArtOfDev.HtmlRenderer.Core;
 using TheArtOfDev.HtmlRenderer.Core.Entities;
 using TheArtOfDev.HtmlRenderer.Avalonia;
@@ -512,7 +513,7 @@ namespace Avalonia.Controls.Html
         protected virtual void InvokeMouseMove()
         {
 
-            _htmlContainer.HandleMouseMove(this, MouseDevice.Instance?.GetPosition(this) ?? default(Point));
+            _htmlContainer.HandleMouseMove(this, (this.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(this) ?? default(Point));
         }
 
         /// <summary>

+ 7 - 0
src/Avalonia.Input/IInputDevice.cs

@@ -1,9 +1,16 @@
 // 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.Input.Raw;
+
 namespace Avalonia.Input
 {
     public interface IInputDevice
     {
+        /// <summary>
+        /// Processes raw event. Is called after preprocessing by InputManager
+        /// </summary>
+        /// <param name="ev"></param>
+        void ProcessRawEvent(RawInputEventArgs ev);
     }
 }

+ 8 - 0
src/Avalonia.Input/IInputRoot.cs

@@ -1,6 +1,8 @@
 // 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 JetBrains.Annotations;
+
 namespace Avalonia.Input
 {
     /// <summary>
@@ -27,5 +29,11 @@ namespace Avalonia.Input
         /// Gets or sets a value indicating whether access keys are shown in the window.
         /// </summary>
         bool ShowAccessKeys { get; set; }
+
+        /// <summary>
+        /// Gets associated mouse device
+        /// </summary>
+        [CanBeNull]
+        IMouseDevice MouseDevice { get; }
     }
 }

+ 2 - 1
src/Avalonia.Input/IKeyboardDevice.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.ComponentModel;
 
 namespace Avalonia.Input
 {
@@ -26,7 +27,7 @@ namespace Avalonia.Input
         Toggled = 2,
     }
 
-    public interface IKeyboardDevice : IInputDevice
+    public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
     {
         IInputElement FocusedElement { get; }
 

+ 1 - 0
src/Avalonia.Input/InputManager.cs

@@ -35,6 +35,7 @@ namespace Avalonia.Input
         public void ProcessInput(RawInputEventArgs e)
         {
             _preProcess.OnNext(e);
+            e.Device?.ProcessRawEvent(e);
             _process.OnNext(e);
             _postProcess.OnNext(e);
         }

+ 3 - 9
src/Avalonia.Input/KeyboardDevice.cs

@@ -16,14 +16,6 @@ namespace Avalonia.Input
     {
         private IInputElement _focusedElement;
 
-        public KeyboardDevice()
-        {
-            InputManager.Process
-                .OfType<RawInputEventArgs>()
-                .Where(e => e.Device == this && !e.Handled)
-                .Subscribe(ProcessRawEvent);
-        }
-
         public event PropertyChangedEventHandler PropertyChanged;
 
         public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService<IKeyboardDevice>();
@@ -77,8 +69,10 @@ namespace Avalonia.Input
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
         }
 
-        private void ProcessRawEvent(RawInputEventArgs e)
+        public void ProcessRawEvent(RawInputEventArgs e)
         {
+            if(e.Handled)
+                return;
             IInputElement element = FocusedElement;
 
             if (element != null)

+ 8 - 23
src/Avalonia.Input/MouseDevice.cs

@@ -20,23 +20,7 @@ namespace Avalonia.Input
         private int _clickCount;
         private Rect _lastClickRect;
         private uint _lastClickTime;
-
-        /// <summary>
-        /// Intializes a new instance of <see cref="MouseDevice"/>.
-        /// </summary>
-        public MouseDevice()
-        {
-            InputManager.Process
-                .OfType<RawMouseEventArgs>()
-                .Where(e => e.Device == this && !e.Handled)
-                .Subscribe(ProcessRawEvent);
-        }
-
-        /// <summary>
-        /// Gets the current mouse device instance.
-        /// </summary>
-        public static IMouseDevice Instance => AvaloniaLocator.Current.GetService<IMouseDevice>();
-
+       
         /// <summary>
         /// Gets the control that is currently capturing by the mouse, if any.
         /// </summary>
@@ -50,12 +34,7 @@ namespace Avalonia.Input
             get;
             protected set;
         }
-
-        /// <summary>
-        /// Gets the application's input manager.
-        /// </summary>
-        public IInputManager InputManager => AvaloniaLocator.Current.GetService<IInputManager>();
-
+        
         /// <summary>
         /// Gets the mouse position, in screen coordinates.
         /// </summary>
@@ -102,6 +81,12 @@ namespace Avalonia.Input
             return root.PointToClient(Position) - p;
         }
 
+        public void ProcessRawEvent(RawInputEventArgs e)
+        {
+            if (!e.Handled && e is RawMouseEventArgs margs)
+                ProcessRawEvent(margs);
+        }
+
         private void ProcessRawEvent(RawMouseEventArgs e)
         {
             Contract.Requires<ArgumentNullException>(e != null);

+ 64 - 33
src/Avalonia.Layout/LayoutManager.cs

@@ -14,8 +14,8 @@ namespace Avalonia.Layout
     /// </summary>
     public class LayoutManager : ILayoutManager
     {
-        private readonly HashSet<ILayoutable> _toMeasure = new HashSet<ILayoutable>();
-        private readonly HashSet<ILayoutable> _toArrange = new HashSet<ILayoutable>();
+        private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
+        private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
         private bool _queued;
         private bool _running;
 
@@ -30,8 +30,18 @@ namespace Avalonia.Layout
             Contract.Requires<ArgumentNullException>(control != null);
             Dispatcher.UIThread.VerifyAccess();
 
-            _toMeasure.Add(control);
-            _toArrange.Add(control);
+            if (!control.IsAttachedToVisualTree)
+            {
+#if DEBUG
+                throw new AvaloniaInternalException(
+                    "LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree.");
+#else
+                return;
+#endif
+            }
+
+            _toMeasure.Enqueue(control);
+            _toArrange.Enqueue(control);
             QueueLayoutPass();
         }
 
@@ -41,7 +51,17 @@ namespace Avalonia.Layout
             Contract.Requires<ArgumentNullException>(control != null);
             Dispatcher.UIThread.VerifyAccess();
 
-            _toArrange.Add(control);
+            if (!control.IsAttachedToVisualTree)
+            {
+#if DEBUG
+                throw new AvaloniaInternalException(
+                    "LayoutManager.InvalidateArrange called on a control that is detached from the visual tree.");
+#else
+                return;
+#endif
+            }
+
+            _toArrange.Enqueue(control);
             QueueLayoutPass();
         }
 
@@ -108,8 +128,12 @@ namespace Avalonia.Layout
         {
             while (_toMeasure.Count > 0)
             {
-                var next = _toMeasure.First();
-                Measure(next);
+                var control = _toMeasure.Dequeue();
+
+                if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
+                {
+                    Measure(control);
+                }
             }
         }
 
@@ -117,53 +141,60 @@ namespace Avalonia.Layout
         {
             while (_toArrange.Count > 0 && _toMeasure.Count == 0)
             {
-                var next = _toArrange.First();
-                Arrange(next);
+                var control = _toArrange.Dequeue();
+
+                if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
+                {
+                    Arrange(control);
+                }
             }
         }
 
         private void Measure(ILayoutable control)
         {
-            var root = control as ILayoutRoot;
-            var parent = control.VisualParent as ILayoutable;
-
-            if (root != null)
-            {
-                root.Measure(root.MaxClientSize);
-            }
-            else if (parent != null)
+            // Controls closest to the visual root need to be arranged first. We don't try to store
+            // ordered invalidation lists, instead we traverse the tree upwards, measuring the
+            // controls closest to the root first. This has been shown by benchmarks to be the
+            // fastest and most memory-efficent algorithm.
+            if (control.VisualParent is ILayoutable parent)
             {
                 Measure(parent);
             }
 
-            if (!control.IsMeasureValid)
+            // If the control being measured has IsMeasureValid == true here then its measure was
+            // handed by an ancestor and can be ignored. The measure may have also caused the
+            // control to be removed.
+            if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
             {
-                control.Measure(control.PreviousMeasure.Value);
+                if (control is ILayoutRoot root)
+                {
+                    root.Measure(Size.Infinity);
+                }
+                else
+                {
+                    control.Measure(control.PreviousMeasure.Value);
+                }
             }
-
-            _toMeasure.Remove(control);
         }
 
         private void Arrange(ILayoutable control)
         {
-            var root = control as ILayoutRoot;
-            var parent = control.VisualParent as ILayoutable;
-
-            if (root != null)
-            {
-                root.Arrange(new Rect(root.DesiredSize));
-            }
-            else if (parent != null)
+            if (control.VisualParent is ILayoutable parent)
             {
                 Arrange(parent);
             }
 
-            if (control.PreviousArrange.HasValue)
+            if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
             {
-                control.Arrange(control.PreviousArrange.Value);
+                if (control is ILayoutRoot root)
+                {
+                    root.Arrange(new Rect(control.DesiredSize));
+                }
+                else
+                {
+                    control.Arrange(control.PreviousArrange.Value);
+                }
             }
-
-            _toArrange.Remove(control);
         }
 
         private void QueueLayoutPass()

+ 12 - 4
src/Avalonia.Layout/Layoutable.cs

@@ -378,8 +378,12 @@ namespace Avalonia.Layout
 
                 IsMeasureValid = false;
                 IsArrangeValid = false;
-                LayoutManager.Instance?.InvalidateMeasure(this);
-                InvalidateVisual();
+
+                if (((ILayoutable)this).IsAttachedToVisualTree)
+                {
+                    LayoutManager.Instance?.InvalidateMeasure(this);
+                    InvalidateVisual();
+                }
             }
         }
 
@@ -393,8 +397,12 @@ namespace Avalonia.Layout
                 Logger.Verbose(LogArea.Layout, this, "Invalidated arrange");
 
                 IsArrangeValid = false;
-                LayoutManager.Instance?.InvalidateArrange(this);
-                InvalidateVisual();
+
+                if (((ILayoutable)this).IsAttachedToVisualTree)
+                {
+                    LayoutManager.Instance?.InvalidateArrange(this);
+                    InvalidateVisual();
+                }
             }
         }
 

+ 1 - 1
src/Avalonia.Visuals/Platform/ILockedFramebuffer.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// DPI of underling screen
         /// </summary>
-        Size Dpi { get; }
+        Vector Dpi { get; }
         
         /// <summary>
         /// Pixel format

+ 11 - 2
src/Avalonia.Visuals/Vector.cs

@@ -52,8 +52,6 @@ namespace Avalonia
             return new Point(a._x, a._y);
         }
 
-        
-
         /// <summary>
         /// Calculates the dot product of two vectors
         /// </summary>
@@ -65,6 +63,17 @@ namespace Avalonia
             return a.X*b.X + a.Y*b.Y;
         }
 
+        /// <summary>
+        /// Scales a vector.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <param name="scale">The scaling factor.</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector operator *(Vector vector, double scale)
+        {
+            return new Vector(vector._x * scale, vector._y * scale);
+        }
+
         /// <summary>
         /// Length of the vector
         /// </summary>

+ 0 - 1
src/Gtk/Avalonia.Gtk/GtkPlatform.cs

@@ -51,7 +51,6 @@ namespace Avalonia.Gtk
                 .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
                 .Bind<IStandardCursorFactory>().ToConstant(CursorFactory.Instance)
                 .Bind<IKeyboardDevice>().ToConstant(GtkKeyboardDevice.Instance)
-                .Bind<IMouseDevice>().ToConstant(GtkMouseDevice.Instance)
                 .Bind<IPlatformSettings>().ToConstant(s_instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
                 .Bind<IRendererFactory>().ToConstant(s_instance)

+ 1 - 1
src/Gtk/Avalonia.Gtk/SurfaceFramebuffer.cs

@@ -48,7 +48,7 @@ namespace Avalonia.Gtk
         public int Height => _surface.Height;
         public int RowBytes => _surface.Stride;
         //TODO: Proper DPI detect
-        public Size Dpi => new Size(96, 96);
+        public Vector Dpi => new Vector(96, 96);
         public PixelFormat Format => PixelFormat.Bgra8888;
     }
 }

+ 3 - 0
src/Gtk/Avalonia.Gtk/TopLevelImpl.cs

@@ -75,6 +75,8 @@ namespace Avalonia.Gtk
             }
         }
 
+        public IMouseDevice MouseDevice => GtkMouseDevice.Instance;
+
         public Avalonia.Controls.WindowState WindowState
         {
             get
@@ -114,6 +116,7 @@ namespace Avalonia.Gtk
 
         public Action Closed { get; set; }
 
+
         public Action Deactivated { get; set; }
 
         public Action<RawInputEventArgs> Input { get; set; }

+ 0 - 1
src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs

@@ -34,7 +34,6 @@ namespace Avalonia.Gtk3
                 .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
                 .Bind<IStandardCursorFactory>().ToConstant(new CursorFactory())
                 .Bind<IKeyboardDevice>().ToConstant(Keyboard)
-                .Bind<IMouseDevice>().ToConstant(Mouse)
                 .Bind<IPlatformSettings>().ToConstant(Instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(Instance)
                 .Bind<ISystemDialogImpl>().ToSingleton<SystemDialog>()

+ 2 - 3
src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs

@@ -52,12 +52,11 @@ namespace Avalonia.Gtk3
         public int RowBytes { get; }
 
         
-        public Size Dpi
+        public Vector Dpi
         {
             get
             {
-                
-                return new Size(96, 96) * _factor;
+                return new Vector(96, 96) * _factor;
             }
         }
 

+ 1 - 0
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@@ -233,6 +233,7 @@ namespace Avalonia.Gtk3
             }
         }
 
+        public IMouseDevice MouseDevice => Gtk3Platform.Mouse;
 
         public double Scaling => (double) 1 / (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1);
 

+ 1 - 0
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@@ -56,6 +56,7 @@ namespace Avalonia.LinuxFramebuffer
         }
 
         public Size ClientSize => _fb.PixelSize;
+        public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice;
         public double Scaling => 1;
         public IEnumerable<object> Surfaces => new object[] {_fb};
         public Action<RawInputEventArgs> Input { get; set; }

+ 3 - 3
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs

@@ -13,16 +13,16 @@ namespace Avalonia.LinuxFramebuffer
 {
     public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable
     {
-        private readonly Size _dpi;
+        private readonly Vector _dpi;
         private int _fd;
         private fb_fix_screeninfo _fixedInfo;
         private fb_var_screeninfo _varInfo;
         private IntPtr _mappedLength;
         private IntPtr _mappedAddress;
 
-        public LinuxFramebuffer(string fileName = null, Size? dpi = null)
+        public LinuxFramebuffer(string fileName = null, Vector? dpi = null)
         {
-            _dpi = dpi ?? new Size(96, 96);
+            _dpi = dpi ?? new Vector(96, 96);
             fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
             _fd = NativeUnsafeMethods.open(fileName, 2, 0);
             if (_fd <= 0)

+ 0 - 1
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -33,7 +33,6 @@ namespace Avalonia.LinuxFramebuffer
             AvaloniaLocator.CurrentMutable
                 .Bind<IStandardCursorFactory>().ToTransient<CursorFactoryStub>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
-                .Bind<IMouseDevice>().ToConstant(MouseDevice)
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)

+ 2 - 2
src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs

@@ -11,7 +11,7 @@ namespace Avalonia.LinuxFramebuffer
         private fb_var_screeninfo _varInfo;
         private readonly IntPtr _address;
 
-        public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Size dpi)
+        public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi)
         {
             _fb = fb;
             _fixedInfo = fixedInfo;
@@ -41,7 +41,7 @@ namespace Avalonia.LinuxFramebuffer
         public int Width => (int)_varInfo.xres;
         public int Height => (int) _varInfo.yres;
         public int RowBytes => (int) _fixedInfo.line_length;
-        public Size Dpi { get; }
+        public Vector Dpi { get; }
         public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
     }
 }

+ 1 - 1
src/Skia/Avalonia.Skia/BitmapImpl.cs

@@ -134,7 +134,7 @@ namespace Avalonia.Skia
             public int Width => _bmp.Width;
             public int Height => _bmp.Height;
             public int RowBytes => _bmp.RowBytes;
-            public Size Dpi { get; } = new Size(96, 96);
+            public Vector Dpi { get; } = new Vector(96, 96);
             public PixelFormat Format => _bmp.ColorType.ToPixelFormat();
         }
 

+ 1 - 1
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -76,7 +76,7 @@ namespace Avalonia.Skia
             canvas.RestoreToCount(0);
             canvas.Save();
             canvas.ResetMatrix();
-            return new DrawingContextImpl(canvas, fb.Dpi.Width, fb.Dpi.Height, visualBrushRenderer, canvas, surface, shim, fb);
+            return new DrawingContextImpl(canvas, fb.Dpi.X, fb.Dpi.Y, visualBrushRenderer, canvas, surface, shim, fb);
         }
     }
 }

+ 2 - 0
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@@ -75,7 +75,9 @@
     </Compile>
     <Compile Include="Direct2D1Platform.cs" />
     <Compile Include="Disposable.cs" />
+    <Compile Include="ExternalRenderTarget.cs" />
     <Compile Include="HwndRenderTarget.cs" />
+    <Compile Include="IExternalDirect2DRenderTargetSurface.cs" />
     <Compile Include="Media\BrushImpl.cs" />
     <Compile Include="Media\BrushWrapper.cs" />
     <Compile Include="Media\DrawingContextImpl.cs" />

+ 10 - 5
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -135,12 +135,17 @@ namespace Avalonia.Direct2D1
 
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
-            var nativeWindow = surfaces?.OfType<IPlatformHandle>().FirstOrDefault();
-            if (nativeWindow != null)
+            foreach (var s in surfaces)
             {
-                if (nativeWindow.HandleDescriptor != "HWND")
-                    throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from " + nativeWindow.HandleDescriptor);
-                return new HwndRenderTarget(nativeWindow);
+                if (s is IPlatformHandle nativeWindow)
+                {
+                    if (nativeWindow.HandleDescriptor != "HWND")
+                        throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from " +
+                                                        nativeWindow.HandleDescriptor);
+                    return new HwndRenderTarget(nativeWindow);
+                }
+                if (s is IExternalDirect2DRenderTargetSurface external)
+                    return new ExternalRenderTarget(external, s_dwfactory);
             }
             throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from any of provided surfaces");
         }

+ 50 - 0
src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs

@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Direct2D1.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using SharpDX;
+using DirectWriteFactory = SharpDX.DirectWrite.Factory;
+
+namespace Avalonia.Direct2D1
+{
+    class ExternalRenderTarget : IRenderTarget
+    {
+        private readonly IExternalDirect2DRenderTargetSurface _externalRenderTargetProvider;
+        private readonly DirectWriteFactory _dwFactory;
+        private SharpDX.Direct2D1.RenderTarget _target;
+        public ExternalRenderTarget(IExternalDirect2DRenderTargetSurface externalRenderTargetProvider,
+            DirectWriteFactory dwFactory)
+        {
+            _externalRenderTargetProvider = externalRenderTargetProvider;
+            _dwFactory = dwFactory;
+        }
+
+        public void Dispose()
+        {
+            _target?.Dispose();
+            _target = null;
+        }
+
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        {
+            _target = _target ?? _externalRenderTargetProvider.CreateRenderTarget();
+            _externalRenderTargetProvider.BeforeDrawing();
+            return new DrawingContextImpl(visualBrushRenderer, _target, _dwFactory, null, () =>
+            {
+                try
+                {
+                    _externalRenderTargetProvider.AfterDrawing();
+                }
+                catch (SharpDXException ex) when ((uint) ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET
+                {
+                    _target?.Dispose();
+                    _target = null;
+                }
+            });
+        }
+    }
+}

+ 15 - 0
src/Windows/Avalonia.Direct2D1/IExternalDirect2DRenderTargetSurface.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Avalonia.Direct2D1
+{
+    public interface IExternalDirect2DRenderTargetSurface
+    {
+        SharpDX.Direct2D1.RenderTarget CreateRenderTarget();
+        void BeforeDrawing();
+        void AfterDrawing();
+    }
+}

+ 6 - 1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -23,6 +23,7 @@ namespace Avalonia.Direct2D1.Media
         private readonly IVisualBrushRenderer _visualBrushRenderer;
         private readonly SharpDX.Direct2D1.RenderTarget _renderTarget;
         private readonly SharpDX.DXGI.SwapChain1 _swapChain;
+        private readonly Action _finishedCallback;
         private SharpDX.DirectWrite.Factory _directWriteFactory;
 
         /// <summary>
@@ -32,15 +33,18 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="renderTarget">The render target to draw to.</param>
         /// <param name="directWriteFactory">The DirectWrite factory.</param>
         /// <param name="swapChain">An optional swap chain associated with this drawing context.</param>
+        /// <param name="finishedCallback">An optional delegate to be called when context is disposed.</param>
         public DrawingContextImpl(
             IVisualBrushRenderer visualBrushRenderer,
             SharpDX.Direct2D1.RenderTarget renderTarget,
             SharpDX.DirectWrite.Factory directWriteFactory,
-            SharpDX.DXGI.SwapChain1 swapChain = null)
+            SharpDX.DXGI.SwapChain1 swapChain = null,
+            Action finishedCallback = null)
         {
             _visualBrushRenderer = visualBrushRenderer;
             _renderTarget = renderTarget;
             _swapChain = swapChain;
+            _finishedCallback = finishedCallback;
             _directWriteFactory = directWriteFactory;
             _swapChain = swapChain;
             _renderTarget.BeginDraw();
@@ -73,6 +77,7 @@ namespace Avalonia.Direct2D1.Media
                 _renderTarget.EndDraw();
 
                 _swapChain?.Present(1, SharpDX.DXGI.PresentFlags.None);
+                _finishedCallback?.Invoke();
             }
             catch (SharpDXException ex) when ((uint)ex.HResult == 0x8899000C) // D2DERR_RECREATE_TARGET
             {

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs

@@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
             public int Width => _lock.Size.Width;
             public int Height => _lock.Size.Height;
             public int RowBytes => _lock.Stride;
-            public Size Dpi { get; } = new Size(96, 96);
+            public Vector Dpi { get; } = new Vector(96, 96);
             public PixelFormat Format => _format;
 
         }

+ 1 - 1
src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Win32.Input
 {
     class WindowsMouseDevice : MouseDevice
     {
-        public new static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
+        public static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
 
         public WindowImpl CurrentWindow
         {

+ 0 - 1
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -76,7 +76,6 @@ namespace Avalonia.Win32
                 .Bind<IClipboard>().ToSingleton<ClipboardImpl>()
                 .Bind<IStandardCursorFactory>().ToConstant(CursorFactory.Instance)
                 .Bind<IKeyboardDevice>().ToConstant(WindowsKeyboardDevice.Instance)
-                .Bind<IMouseDevice>().ToConstant(WindowsMouseDevice.Instance)
                 .Bind<IPlatformSettings>().ToConstant(s_instance)
                 .Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
                 .Bind<IRenderLoop>().ToConstant(new RenderLoop(60))

+ 3 - 3
src/Windows/Avalonia.Win32/WindowFramebuffer.cs

@@ -39,7 +39,7 @@ namespace Avalonia.Win32
         public int RowBytes => Width * 4;
         public PixelFormat Format => PixelFormat.Bgra8888;
 
-        public Size Dpi
+        public Vector Dpi
         {
             get
             {
@@ -56,10 +56,10 @@ namespace Avalonia.Win32
                             out dpix,
                             out dpiy) == 0)
                     {
-                        return new Size(dpix, dpiy);
+                        return new Vector(dpix, dpiy);
                     }
                 }
-                return new Size(96, 96);
+                return new Vector(96, 96);
             }
         }
 

+ 2 - 0
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -133,6 +133,8 @@ namespace Avalonia.Win32
             }
         }
 
+        public IMouseDevice MouseDevice => WindowsMouseDevice.Instance;
+
         public WindowState WindowState
         {
             get

+ 2 - 2
src/iOS/Avalonia.iOS/EmulatedFramebuffer.cs

@@ -24,7 +24,7 @@ namespace Avalonia.iOS
             Width = (int) frame.Width * factor;
             Height = (int) frame.Height * factor;
             RowBytes = Width * 4;
-            Dpi = new Size(96, 96) * factor;
+            Dpi = new Vector(96, 96) * factor;
             Format = PixelFormat.Rgba8888;
             Address = Marshal.AllocHGlobal(Height * RowBytes);
         }
@@ -53,7 +53,7 @@ namespace Avalonia.iOS
         public int Width { get; }
         public int Height { get; }
         public int RowBytes { get; }
-        public Size Dpi { get; }
+        public Vector Dpi { get; }
         public PixelFormat Format { get; }
     }
 }

+ 2 - 0
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@@ -61,6 +61,8 @@ namespace Avalonia.iOS
 
         public Size ClientSize => Bounds.Size.ToAvalonia();
 
+        public IMouseDevice MouseDevice => iOSPlatform.MouseDevice;
+        
         public override void Draw(CGRect rect)
         {
             Paint?.Invoke(new Rect(rect.X, rect.Y, rect.Width, rect.Height));

+ 0 - 1
src/iOS/Avalonia.iOS/iOSPlatform.cs

@@ -57,7 +57,6 @@ namespace Avalonia.iOS
                 //.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
                 .Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
                 .Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
-                .Bind<IMouseDevice>().ToConstant(MouseDevice)
                 .Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
                 .Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
                 .Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)

+ 1 - 0
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 134 - 0
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs

@@ -0,0 +1,134 @@
+using System;
+using System.Linq;
+using Avalonia.Collections;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Collections
+{
+    public class AvaloniaListExtenionsTests
+    {
+        [Fact]
+        public void CreateDerivedList_Creates_Initial_Items()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Add()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Add(4);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Insert()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Insert(1, 4);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Remove()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Remove(2);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_RemoveRange()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.RemoveRange(1, 2);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Move()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Move(2, 0);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_MoveRange()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.MoveRange(1, 2, 0);
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Replace()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source[1] = 4;
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        [Fact]
+        public void CreateDerivedList_Handles_Clear()
+        {
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var target = source.CreateDerivedList(x => new Wrapper(x));
+
+            source.Clear();
+
+            var result = target.Select(x => x.Value).ToList();
+
+            Assert.Equal(source, result);
+        }
+
+        private class Wrapper
+        {
+            public Wrapper(int value)
+            {
+                Value = value;
+            }
+
+            public int Value { get; }
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.Base.UnitTests/Properties/AssemblyInfo.cs

@@ -7,4 +7,4 @@ using Xunit;
 [assembly: AssemblyTitle("Avalonia.UnitTests")]
 
 // Don't run tests in parallel.
-[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: CollectionBehavior(MaxParallelThreads = 1)]

+ 2 - 1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -49,6 +49,7 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Layout\Measure.cs" />
     <Compile Include="Styling\ApplyStyling.cs" />
     <Compile Include="Program.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
@@ -100,7 +101,7 @@
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.9.2" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
   </ItemGroup>
   <Import Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
 </Project>

+ 65 - 0
tests/Avalonia.Benchmarks/Layout/Measure.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.UnitTests;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Layout
+{
+    [MemoryDiagnoser]
+    public class Measure : IDisposable
+    {
+        private IDisposable _app;
+        private TestRoot root;
+        private List<Control> controls = new List<Control>();
+
+        public Measure()
+        {
+            _app = UnitTestApplication.Start(TestServices.RealLayoutManager);
+
+            var panel = new StackPanel();
+            root = new TestRoot { Child = panel };
+            controls.Add(panel);
+            CreateChildren(panel, 3, 5);
+            LayoutManager.Instance.ExecuteInitialLayoutPass(root);
+        }
+
+        public void Dispose()
+        {
+            _app.Dispose();
+        }
+
+        [Benchmark]
+        public void Remeasure_Half()
+        {
+            var random = new Random(1);
+
+            foreach (var control in controls)
+            {
+                if (random.Next(2) == 0)
+                {
+                    control.InvalidateMeasure();
+                }
+            }
+
+            LayoutManager.Instance.ExecuteLayoutPass();
+        }
+
+        private void CreateChildren(IPanel parent, int childCount, int iterations)
+        {
+            for (var i = 0; i < childCount; ++i)
+            {
+                var control = new StackPanel();
+                parent.Children.Add(control);
+
+                if (iterations > 0)
+                {
+                    CreateChildren(control, childCount, iterations - 1);
+                }
+
+                controls.Add(control);
+            }
+        }
+    }
+}

+ 1 - 0
tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs

@@ -11,6 +11,7 @@ using Avalonia.VisualTree;
 
 namespace Avalonia.Benchmarks.Styling
 {
+    [MemoryDiagnoser]
     public class ApplyStyling : IDisposable
     {
         private IDisposable _app;

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

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

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>

+ 1 - 0
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\XUnit.props" />

+ 1 - 0
tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 265 - 7
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@@ -2,25 +2,276 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using Avalonia.Controls;
+using Avalonia.UnitTests;
+using System;
 using Xunit;
+using System.Collections.Generic;
 
 namespace Avalonia.Layout.UnitTests
 {
     public class LayoutManagerTests
     {
         [Fact]
-        public void Invalidating_Child_Should_Remeasure_Parent()
+        public void Measures_And_Arranges_InvalidateMeasured_Control()
         {
-            var layoutManager = new LayoutManager();
+            var target = new LayoutManager();
 
-            using (AvaloniaLocator.EnterScope())
+            using (Start(target))
             {
-                AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
+                var control = new LayoutTestControl();
+                var root = new LayoutTestRoot { Child = control };
+
+                target.ExecuteInitialLayoutPass(root);
+                control.Measured = control.Arranged = false;
+
+                control.InvalidateMeasure();
+                target.ExecuteLayoutPass();
+
+                Assert.True(control.Measured);
+                Assert.True(control.Arranged);
+            }
+        }
+
+        [Fact]
+        public void Arranges_InvalidateArranged_Control()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                var control = new LayoutTestControl();
+                var root = new LayoutTestRoot { Child = control };
+
+                target.ExecuteInitialLayoutPass(root);
+                control.Measured = control.Arranged = false;
+
+                control.InvalidateArrange();
+                target.ExecuteLayoutPass();
+
+                Assert.False(control.Measured);
+                Assert.True(control.Arranged);
+            }
+        }
+
+        [Fact]
+        public void Measures_Parent_Of_Newly_Added_Control()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                var control = new LayoutTestControl();
+                var root = new LayoutTestRoot();
+
+                target.ExecuteInitialLayoutPass(root);
+                root.Child = control;
+                root.Measured = root.Arranged = false;
+
+                target.ExecuteLayoutPass();
+
+                Assert.True(root.Measured);
+                Assert.True(root.Arranged);
+                Assert.True(control.Measured);
+                Assert.True(control.Arranged);
+            }
+        }
+
+        [Fact]
+        public void Measures_In_Correct_Order()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                LayoutTestControl control1;
+                LayoutTestControl control2;
+                var root = new LayoutTestRoot
+                {
+                    Child = control1 = new LayoutTestControl
+                    {
+                        Child = control2 = new LayoutTestControl(),
+                    }
+                };
+
+
+                var order = new List<ILayoutable>();
+                Size MeasureOverride(ILayoutable control, Size size)
+                {
+                    order.Add(control);
+                    return new Size(10, 10);
+                }
+
+                root.DoMeasureOverride = MeasureOverride;
+                control1.DoMeasureOverride = MeasureOverride;
+                control2.DoMeasureOverride = MeasureOverride;
+                target.ExecuteInitialLayoutPass(root);
+
+                control2.InvalidateMeasure();
+                control1.InvalidateMeasure();
+                root.InvalidateMeasure();
+
+                order.Clear();
+                target.ExecuteLayoutPass();
+
+                Assert.Equal(new ILayoutable[] { root, control1, control2 }, order);
+            }
+        }
+
+        [Fact]
+        public void Measures_Root_And_Grandparent_In_Correct_Order()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                LayoutTestControl control1;
+                LayoutTestControl control2;
+                var root = new LayoutTestRoot
+                {
+                    Child = control1 = new LayoutTestControl
+                    {
+                        Child = control2 = new LayoutTestControl(),
+                    }
+                };
+
+
+                var order = new List<ILayoutable>();
+                Size MeasureOverride(ILayoutable control, Size size)
+                {
+                    order.Add(control);
+                    return new Size(10, 10);
+                }
+
+                root.DoMeasureOverride = MeasureOverride;
+                control1.DoMeasureOverride = MeasureOverride;
+                control2.DoMeasureOverride = MeasureOverride;
+                target.ExecuteInitialLayoutPass(root);
+
+                control2.InvalidateMeasure();
+                root.InvalidateMeasure();
+
+                order.Clear();
+                target.ExecuteLayoutPass();
+
+                Assert.Equal(new ILayoutable[] { root, control2 }, order);
+            }
+        }
+
+        [Fact]
+        public void Doesnt_Measure_Non_Invalidated_Root()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                var control = new LayoutTestControl();
+                var root = new LayoutTestRoot { Child = control };
+
+                target.ExecuteInitialLayoutPass(root);
+                root.Measured = root.Arranged = false;
+                control.Measured = control.Arranged = false;
+
+                control.InvalidateMeasure();
+                target.ExecuteLayoutPass();
+
+                Assert.False(root.Measured);
+                Assert.False(root.Arranged);
+                Assert.True(control.Measured);
+                Assert.True(control.Arranged);
+            }
+        }
+
+        [Fact]
+        public void Doesnt_Measure_Removed_Control()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                var control = new LayoutTestControl();
+                var root = new LayoutTestRoot { Child = control };
+
+                target.ExecuteInitialLayoutPass(root);
+                control.Measured = control.Arranged = false;
+
+                control.InvalidateMeasure();
+                root.Child = null;
+                target.ExecuteLayoutPass();
+
+                Assert.False(control.Measured);
+                Assert.False(control.Arranged);
+            }
+        }
+
+        [Fact]
+        public void Measures_Root_With_Infinity()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                var root = new LayoutTestRoot();
+                var availableSize = default(Size);
+
+                // Should not measure with this size.
+                root.MaxClientSize = new Size(123, 456);
+
+                root.DoMeasureOverride = (_, s) =>
+                {
+                    availableSize = s;
+                    return new Size(100, 100);
+                };
+
+                target.ExecuteInitialLayoutPass(root);
+
+                Assert.Equal(Size.Infinity, availableSize);
+            }
+        }
+
+        [Fact]
+        public void Arranges_Root_With_DesiredSize()
+        {
+            var target = new LayoutManager();
+ 
+            using (Start(target))
+            {
+                var root = new LayoutTestRoot
+                {
+                    Width = 100,
+                    Height = 100,
+                };
+ 
+                var arrangeSize = default(Size);
+ 
+                root.DoArrangeOverride = (_, s) =>
+                {
+                    arrangeSize = s;
+                    return s;
+                };
+ 
+                target.ExecuteInitialLayoutPass(root);
+                Assert.Equal(new Size(100, 100), arrangeSize);
+ 
+                root.Width = 120;
+ 
+                target.ExecuteLayoutPass();
+                Assert.Equal(new Size(120, 100), arrangeSize);
+            }
+        }
+
+        [Fact]
+        public void Invalidating_Child_Remeasures_Parent()
+        {
+            var target = new LayoutManager();
+
+            using (Start(target))
+            {
+                AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(target);
 
                 Border border;
                 StackPanel panel;
 
-                var root = new TestLayoutRoot
+                var root = new LayoutTestRoot
                 {
                     Child = panel = new StackPanel
                     {
@@ -31,15 +282,22 @@ namespace Avalonia.Layout.UnitTests
                     }
                 };
 
-                layoutManager.ExecuteInitialLayoutPass(root);
+                target.ExecuteInitialLayoutPass(root);
                 Assert.Equal(new Size(0, 0), root.DesiredSize);
 
                 border.Width = 100;
                 border.Height = 100;
 
-                layoutManager.ExecuteLayoutPass();
+                target.ExecuteLayoutPass();
                 Assert.Equal(new Size(100, 100), panel.DesiredSize);
             }                
         }
+
+        private IDisposable Start(LayoutManager layoutManager)
+        {
+            var result = AvaloniaLocator.EnterScope();
+            AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
+            return result;
+        }
     }
 }

+ 29 - 0
tests/Avalonia.Layout.UnitTests/LayoutTestControl.cs

@@ -0,0 +1,29 @@
+using System;
+using Avalonia.Controls;
+
+namespace Avalonia.Layout.UnitTests
+{
+    internal class LayoutTestControl : Decorator
+    {
+        public bool Measured { get; set; }
+        public bool Arranged { get; set; }
+        public Func<ILayoutable, Size, Size> DoMeasureOverride { get; set; }
+        public Func<ILayoutable, Size, Size> DoArrangeOverride { get; set; }
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            Measured = true;
+            return DoMeasureOverride != null ?
+                DoMeasureOverride(this, availableSize) :
+                base.MeasureOverride(availableSize);
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            Arranged = true;
+            return DoArrangeOverride != null ?
+                DoArrangeOverride(this, finalSize) :
+                base.ArrangeOverride(finalSize);
+        }
+    }
+}

+ 43 - 0
tests/Avalonia.Layout.UnitTests/LayoutTestRoot.cs

@@ -0,0 +1,43 @@
+// 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.UnitTests;
+
+namespace Avalonia.Layout.UnitTests
+{
+    internal class LayoutTestRoot : TestRoot, ILayoutable
+    {
+        public bool Measured { get; set; }
+        public bool Arranged { get; set; }
+        public Func<ILayoutable, Size, Size> DoMeasureOverride { get; set; }
+        public Func<ILayoutable, Size, Size> DoArrangeOverride { get; set; }
+
+        void ILayoutable.Measure(Size availableSize)
+        {
+            Measured = true;
+            Measure(availableSize);
+        }
+
+        void ILayoutable.Arrange(Rect rect)
+        {
+            Arranged = true;
+            Arrange(rect);
+        }
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            return DoMeasureOverride != null ?
+                DoMeasureOverride(this, availableSize) :
+                base.MeasureOverride(availableSize);
+        }
+
+        protected override Size ArrangeOverride(Size finalSize)
+        {
+            Arranged = true;
+            return DoArrangeOverride != null ?
+                DoArrangeOverride(this, finalSize) :
+                base.ArrangeOverride(finalSize);
+        }
+    }
+}

+ 90 - 0
tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

@@ -0,0 +1,90 @@
+using System;
+using Avalonia.Controls;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Layout.UnitTests
+{
+    public class LayoutableTests
+    {
+        [Fact]
+        public void Only_Calls_LayoutManager_InvalidateMeasure_Once()
+        {
+            var target = new Mock<ILayoutManager>();
+
+            using (Start(target.Object))
+            {
+                var control = new Decorator();
+                var root = new LayoutTestRoot { Child = control };
+
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(root.DesiredSize));
+                target.ResetCalls();
+
+                control.InvalidateMeasure();
+                control.InvalidateMeasure();
+
+                target.Verify(x => x.InvalidateMeasure(control), Times.Once());
+            }
+        }
+
+        [Fact]
+        public void Only_Calls_LayoutManager_InvalidateArrange_Once()
+        {
+            var target = new Mock<ILayoutManager>();
+
+            using (Start(target.Object))
+            {
+                var control = new Decorator();
+                var root = new LayoutTestRoot { Child = control };
+
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(root.DesiredSize));
+                target.ResetCalls();
+
+                control.InvalidateArrange();
+                control.InvalidateArrange();
+
+                target.Verify(x => x.InvalidateArrange(control), Times.Once());
+            }
+        }
+
+        [Fact]
+        public void Attaching_Control_To_Tree_Invalidates_Parent_Measure()
+        {
+            var target = new Mock<ILayoutManager>();
+
+            using (Start(target.Object))
+            {
+                var control = new Decorator();
+                var root = new LayoutTestRoot { Child = control };
+
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(root.DesiredSize));
+                Assert.True(control.IsMeasureValid);
+
+                root.Child = null;
+                root.Measure(Size.Infinity);
+                root.Arrange(new Rect(root.DesiredSize));
+
+                Assert.False(control.IsMeasureValid);
+                Assert.True(root.IsMeasureValid);
+
+                target.ResetCalls();
+
+                root.Child = control;
+
+                Assert.False(root.IsMeasureValid);
+                Assert.False(control.IsMeasureValid);
+                target.Verify(x => x.InvalidateMeasure(root), Times.Once());
+            }
+        }
+
+        private IDisposable Start(ILayoutManager layoutManager)
+        {
+            var result = AvaloniaLocator.EnterScope();
+            AvaloniaLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
+            return result;
+        }
+    }
+}

+ 0 - 24
tests/Avalonia.Layout.UnitTests/TestLayoutRoot.cs

@@ -1,24 +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;
-
-namespace Avalonia.Layout.UnitTests
-{
-    internal class TestLayoutRoot : Decorator, ILayoutRoot
-    {
-        public TestLayoutRoot()
-        {
-            ClientSize = new Size(500, 500);
-        }
-
-        public Size ClientSize
-        {
-            get;
-            set;
-        }
-
-        public Size MaxClientSize => Size.Infinity;
-        public double LayoutScaling => 1;
-    }
-}

+ 8 - 0
tests/Avalonia.LeakTests/ControlTests.cs

@@ -368,6 +368,14 @@ namespace Avalonia.LeakTests
             public void Resized(Size size)
             {
             }
+
+            public void Start()
+            {
+            }
+
+            public void Stop()
+            {
+            }
         }
     }
 }

+ 1 - 0
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 24 - 6
tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs

@@ -183,7 +183,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 result);
         }
 
-        [Fact]
+        [Fact(Skip="Result is not always AggregateException.")]
         public async void Should_Return_BindingNotification_For_Invalid_FallbackValue()
         {
 #if NET461
@@ -203,13 +203,13 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal(
                 new BindingNotification(
                     new AggregateException(
-                        new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+                        new InvalidCastException("'foo' is not a valid number."),
                         new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
                     BindingErrorType.Error),
                 result);
         }
 
-        [Fact]
+        [Fact(Skip="Result is not always AggregateException.")]
         public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
         {
 #if NET461
@@ -229,7 +229,7 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal(
                 new BindingNotification(
                     new AggregateException(
-                        new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+                        new InvalidCastException("'foo' is not a valid number."),
                         new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
                     BindingErrorType.Error),
                 result);
@@ -286,6 +286,12 @@ namespace Avalonia.Markup.UnitTests.Data
         [Fact]
         public void Should_Pass_ConverterParameter_To_Convert()
         {
+#if NET461
+            Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+#else
+            CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+#endif
+
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(
@@ -296,12 +302,18 @@ namespace Avalonia.Markup.UnitTests.Data
 
             target.Subscribe(_ => { });
 
-            converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
+            converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.InvariantCulture));
         }
 
         [Fact]
         public void Should_Pass_ConverterParameter_To_ConvertBack()
         {
+#if NET461
+            Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+#else
+            CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+#endif
+
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(
@@ -312,12 +324,18 @@ namespace Avalonia.Markup.UnitTests.Data
 
             target.OnNext("bar");
 
-            converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
+            converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.InvariantCulture));
         }
 
         [Fact]
         public void Should_Handle_DataValidation()
         {
+#if NET461
+            Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+#else
+            CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
+#endif
+
             var data = new Class1 { DoubleValue = 5.6 };
             var converter = new Mock<IValueConverter>();
             var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Properties/AssemblyInfo.cs

@@ -37,4 +37,4 @@ using Xunit;
 [assembly: AssemblyFileVersion("1.0.0.0")]
 
 // Don't run tests in parallel.
-[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: CollectionBehavior(MaxParallelThreads = 1)]

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 1
tests/Avalonia.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs

@@ -7,4 +7,4 @@ using Xunit;
 [assembly: AssemblyTitle("Avalonia.Markup.Xaml.UnitTests")]
 
 // Don't run tests in parallel.
-[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: CollectionBehavior(MaxParallelThreads = 1)]

+ 1 - 1
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@@ -43,7 +43,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
 
             public IntPtr Address { get; }
 
-            public Size Dpi { get; } = new Size(96, 96);
+            public Vector Dpi { get; } = new Vector(96, 96);
 
             public PixelFormat Format { get; }
 

+ 1 - 0
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\Moq.props" />

+ 1 - 3
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@@ -2,6 +2,7 @@
   <PropertyGroup>
     <TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <OutputType>Library</OutputType>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
@@ -51,8 +52,5 @@
   <Import Project="..\..\build\Moq.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\XUnit.props" />
-  <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.1'">
-      <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
-  </ItemGroup>
   <Import Condition="'$(TargetFramework)' == 'net461'" Project="$(MSBuildThisFileDirectory)..\..\src\Shared\nuget.workaround.targets" />
 </Project>

+ 3 - 1
tests/Avalonia.UnitTests/TestRoot.cs

@@ -44,7 +44,7 @@ namespace Avalonia.UnitTests
 
         public Size ClientSize { get; set; } = new Size(100, 100);
 
-        public Size MaxClientSize => Size.Infinity;
+        public Size MaxClientSize { get; set; } = Size.Infinity;
 
         public double LayoutScaling => 1;
 
@@ -62,6 +62,8 @@ namespace Avalonia.UnitTests
 
         public IInputElement PointerOverElement { get; set; }
 
+        public IMouseDevice MouseDevice { get; set; }
+
         public bool ShowAccessKeys { get; set; }
 
         public IRenderTarget CreateRenderTarget() => _renderTarget;

+ 4 - 0
tools/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+    <package id="Cake" version="0.18.0" />
+</packages>