Pārlūkot izejas kodu

Merge branch 'master' into fixes/2350-stackpanel-layout

Steven Kirk 6 gadi atpakaļ
vecāks
revīzija
71f4cb93fb
85 mainītis faili ar 1052 papildinājumiem un 263 dzēšanām
  1. 2 2
      azure-pipelines.yml
  2. 3 3
      build/SharedVersion.props
  3. 8 0
      nukebuild/Build.cs
  4. 1 1
      nukebuild/Numerge
  5. 1 0
      samples/BindingDemo/App.xaml.cs
  6. 1 0
      samples/ControlCatalog.Desktop/Program.cs
  7. 1 0
      samples/ControlCatalog.NetCore/Program.cs
  8. 1 0
      samples/RenderDemo/App.xaml.cs
  9. 3 0
      samples/RenderDemo/MainWindow.xaml
  10. 119 0
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  11. 1 0
      samples/VirtualizationDemo/Program.cs
  12. 2 0
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  13. 2 2
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  14. 1 2
      src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
  15. 45 24
      src/Avalonia.Controls/Button.cs
  16. 21 0
      src/Avalonia.Controls/Grid.cs
  17. 2 2
      src/Avalonia.Controls/ListBox.cs
  18. 29 6
      src/Avalonia.Controls/MenuItem.cs
  19. 4 1
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  20. 2 2
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  21. 42 30
      src/Avalonia.Controls/Shapes/Shape.cs
  22. 3 5
      src/Avalonia.Controls/TreeView.cs
  23. 13 1
      src/Avalonia.Controls/Window.cs
  24. 6 1
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  25. 8 0
      src/Avalonia.ReactiveUI/Attributes.cs
  26. 17 1
      src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs
  27. 1 1
      src/Avalonia.ReactiveUI/ReactiveUserControl.cs
  28. 1 1
      src/Avalonia.ReactiveUI/ReactiveWindow.cs
  29. 6 2
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  30. 1 0
      src/Avalonia.Styling/StyledElement.cs
  31. 12 14
      src/Avalonia.Themes.Default/ScrollBar.xaml
  32. 4 6
      src/Avalonia.Visuals/Media/BrushExtensions.cs
  33. 7 0
      src/Avalonia.Visuals/Media/DrawingContext.cs
  34. 1 30
      src/Avalonia.Visuals/Media/EllipseGeometry.cs
  35. 1 9
      src/Avalonia.Visuals/Media/LineGeometry.cs
  36. 31 41
      src/Avalonia.Visuals/Media/Pen.cs
  37. 1 2
      src/Avalonia.Visuals/Media/PenLineCap.cs
  38. 10 18
      src/Avalonia.Visuals/Media/RectangleGeometry.cs
  39. 7 0
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  40. 22 0
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  41. 39 0
      src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs
  42. 9 0
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  43. 2 1
      src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
  44. 1 1
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github
  45. 42 0
      src/Skia/Avalonia.Skia/CustomRenderTarget.cs
  46. 11 15
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  47. 25 0
      src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs
  48. 1 1
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  49. 1 1
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  50. 1 1
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  51. 1 1
      src/Skia/Avalonia.Skia/GlRenderTarget.cs
  52. 26 0
      src/Skia/Avalonia.Skia/ICustomSkiaGpu.cs
  53. 29 0
      src/Skia/Avalonia.Skia/ICustomSkiaRenderSession.cs
  54. 19 0
      src/Skia/Avalonia.Skia/ICustomSkiaRenderTarget.cs
  55. 10 0
      src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs
  56. 1 1
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  57. 29 0
      src/Skia/Avalonia.Skia/LineGeometryImpl.cs
  58. 32 6
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  59. 25 0
      src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs
  60. 3 2
      src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs
  61. 19 0
      src/Skia/Avalonia.Skia/SkiaOptions.cs
  62. 8 2
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  63. 1 1
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  64. 1 1
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  65. 1 1
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  66. 1 1
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  67. 4 4
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  68. 3 0
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  69. 27 0
      src/Windows/Avalonia.Direct2D1/Media/EllipseGeometryImpl.cs
  70. 1 1
      src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs
  71. 27 0
      src/Windows/Avalonia.Direct2D1/Media/LineGeometryImpl.cs
  72. 26 0
      src/Windows/Avalonia.Direct2D1/Media/RectangleGeometryImpl.cs
  73. 5 3
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  74. 1 1
      src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs
  75. 44 2
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  76. 54 0
      tests/Avalonia.Controls.UnitTests/MenuItemTests.cs
  77. 45 0
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  78. 1 0
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
  79. 1 0
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
  80. 1 7
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  81. 1 2
      tests/Avalonia.RenderTests/Shapes/PolylineTests.cs
  82. 15 0
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  83. 15 0
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  84. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png
  85. BIN
      tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png

+ 2 - 2
azure-pipelines.yml

@@ -32,7 +32,7 @@ jobs:
      
 - job: macOS
   pool:
-    vmImage: 'xcode9-macos10.13'
+    vmImage: 'macOS-10.14'
   steps:
   - task: DotNetCoreInstaller@0
     inputs:
@@ -49,7 +49,7 @@ jobs:
     inputs:
       actions: 'build'
       scheme: ''
-      sdk: 'macosx10.13'
+      sdk: 'macosx10.14'
       configuration: 'Release'
       xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
       xcodeVersion: 'default' # Options: 8, 9, default, specifyPath

+ 3 - 3
build/SharedVersion.props

@@ -2,8 +2,8 @@
   xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <Product>Avalonia</Product>
-    <Version>0.7.1</Version>
-    <Copyright>Copyright 2018 &#169; The AvaloniaUI Project</Copyright>
+    <Version>0.8.1</Version>
+    <Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
     <PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
     <PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>
     <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
@@ -11,4 +11,4 @@
     <NoWarn>CS1591</NoWarn>
     <LangVersion>latest</LangVersion>
   </PropertyGroup>
-</Project>
+</Project>

+ 8 - 0
nukebuild/Build.cs

@@ -122,6 +122,14 @@ partial class Build : NukeBuild
         
         foreach(var fw in frameworks)
         {
+            if (fw.StartsWith("net4")
+                && RuntimeInformation.IsOSPlatform(OSPlatform.Linux) 
+                && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1")
+            {
+                Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969");
+                continue;
+            }
+
             Information("Running for " + fw);
             DotNetTest(c =>
             {

+ 1 - 1
nukebuild/Numerge

@@ -1 +1 @@
-Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8
+Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5

+ 1 - 0
samples/BindingDemo/App.xaml.cs

@@ -3,6 +3,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Logging.Serilog;
 using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
 using Serilog;
 
 namespace BindingDemo

+ 1 - 0
samples/ControlCatalog.Desktop/Program.cs

@@ -4,6 +4,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Logging.Serilog;
 using Avalonia.Platform;
+using Avalonia.ReactiveUI;
 using Serilog;
 
 namespace ControlCatalog

+ 1 - 0
samples/ControlCatalog.NetCore/Program.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Threading;
 using Avalonia;
 using Avalonia.Skia;
+using Avalonia.ReactiveUI;
 
 namespace ControlCatalog.NetCore
 {

+ 1 - 0
samples/RenderDemo/App.xaml.cs

@@ -4,6 +4,7 @@
 using Avalonia;
 using Avalonia.Logging.Serilog;
 using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
 
 namespace RenderDemo
 {

+ 3 - 0
samples/RenderDemo/MainWindow.xaml

@@ -33,6 +33,9 @@
       <TabItem Header="Drawing">
         <pages:DrawingPage/>
       </TabItem>
+      <TabItem Header="SkCanvas">
+        <pages:CustomSkiaPage/>
+      </TabItem>
     </TabControl>
   </DockPanel>
 </Window>

+ 119 - 0
samples/RenderDemo/Pages/CustomSkiaPage.cs

@@ -0,0 +1,119 @@
+using System;
+using System.Diagnostics;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using Avalonia.Threading;
+using SkiaSharp;
+
+namespace RenderDemo.Pages
+{
+    public class CustomSkiaPage : Control
+    {
+        public CustomSkiaPage()
+        {
+            ClipToBounds = true;
+        }
+        
+        class CustomDrawOp : ICustomDrawOperation
+        {
+            private readonly FormattedText _noSkia;
+
+            public CustomDrawOp(Rect bounds, FormattedText noSkia)
+            {
+                _noSkia = noSkia;
+                Bounds = bounds;
+            }
+            
+            public void Dispose()
+            {
+                // No-op
+            }
+
+            public Rect Bounds { get; }
+            public bool HitTest(Point p) => false;
+            public bool Equals(ICustomDrawOperation other) => false;
+            static Stopwatch St = Stopwatch.StartNew();
+            public void Render(IDrawingContextImpl context)
+            {
+                var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
+                if (canvas == null)
+                    context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl);
+                else
+                {
+                    canvas.Save();
+                    // create the first shader
+                    var colors = new SKColor[] {
+                        new SKColor(0, 255, 255),
+                        new SKColor(255, 0, 255),
+                        new SKColor(255, 255, 0),
+                        new SKColor(0, 255, 255)
+                    };
+
+                    var sx = Animate(100, 2, 10);
+                    var sy = Animate(1000, 5, 15);
+                    var lightPosition = new SKPoint(
+                        (float)(Bounds.Width / 2 + Math.Cos(St.Elapsed.TotalSeconds) * Bounds.Width / 4),
+                        (float)(Bounds.Height / 2 + Math.Sin(St.Elapsed.TotalSeconds) * Bounds.Height / 4));
+                    using (var sweep =
+                        SKShader.CreateSweepGradient(new SKPoint((int)Bounds.Width / 2, (int)Bounds.Height / 2), colors,
+                            null)) 
+                    using(var turbulence = SKShader.CreatePerlinNoiseFractalNoise(0.05f, 0.05f, 4, 0))
+                    using(var shader = SKShader.CreateCompose(sweep, turbulence, SKBlendMode.SrcATop))
+                    using(var blur = SKImageFilter.CreateBlur(Animate(100, 2, 10), Animate(100, 5, 15)))
+                    using (var paint = new SKPaint
+                    {
+                        Shader = shader,
+                        ImageFilter = blur
+                    })
+                        canvas.DrawPaint(paint);
+                    
+                    using (var pseudoLight = SKShader.CreateRadialGradient(
+                        lightPosition,
+                        (float) (Bounds.Width/3),
+                        new [] { 
+                            new SKColor(255, 200, 200, 100), 
+                            SKColors.Transparent,
+                            new SKColor(40,40,40, 220), 
+                            new SKColor(20,20,20, (byte)Animate(100, 200,220)) },
+                        new float[] { 0.3f, 0.3f, 0.8f, 1 },
+                        SKShaderTileMode.Clamp))
+                    using (var paint = new SKPaint
+                    {
+                        Shader = pseudoLight
+                    })
+                        canvas.DrawPaint(paint);
+                    canvas.Restore();
+                }
+            }    
+            static int Animate(int d, int from, int to)
+            {
+                var ms = (int)(St.ElapsedMilliseconds / d);
+                var diff = to - from;
+                var range = diff * 2;
+                var v = ms % range;
+                if (v > diff)
+                    v = range - v;
+                var rv = v + from;
+                if (rv < from || rv > to)
+                    throw new Exception("WTF");
+                return rv;
+            }
+        }
+
+
+        
+        public override void Render(DrawingContext context)
+        {
+            var noSkia = new FormattedText()
+            {
+                Text = "Current rendering API is not Skia"
+            };
+            context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
+            Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
+        }
+    }
+}

+ 1 - 0
samples/VirtualizationDemo/Program.cs

@@ -5,6 +5,7 @@ using System;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Logging.Serilog;
+using Avalonia.ReactiveUI;
 using Serilog;
 
 namespace VirtualizationDemo

+ 2 - 0
src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs

@@ -19,6 +19,7 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <typeparam name="TTarget">The type of the target.</typeparam>
         /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
+        /// <typeparam name="TSubscriber">The type of the subscriber.</typeparam>
         /// <param name="target">The event source.</param>
         /// <param name="eventName">The name of the event.</param>
         /// <param name="subscriber">The subscriber.</param>
@@ -40,6 +41,7 @@ namespace Avalonia.Utilities
         /// Unsubscribes from an event.
         /// </summary>
         /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
+        /// <typeparam name="TSubscriber">The type of the subscriber.</typeparam>
         /// <param name="target">The event source.</param>
         /// <param name="eventName">The name of the event.</param>
         /// <param name="subscriber">The subscriber.</param>

+ 2 - 2
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -927,7 +927,7 @@ namespace Avalonia.Collections
         /// <remarks>
         /// <p>
         /// Clear a sort criteria by assigning SortDescription.Empty to this property.
-        /// One or more sort criteria in form of <seealso cref="SortDescription"/>
+        /// One or more sort criteria in form of <seealso cref="DataGridSortDescription"/>
         /// can be used, each specifying a property and direction to sort by.
         /// </p>
         /// </remarks>
@@ -4312,4 +4312,4 @@ namespace Avalonia.Collections
             }
         }       
     }
-}
+}

+ 1 - 2
src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs

@@ -8,7 +8,6 @@ using Avalonia.Metadata;
 [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")]
 [assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
 
-[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
-[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]

+ 45 - 24
src/Avalonia.Controls/Button.cs

@@ -7,6 +7,7 @@ using System.Windows.Input;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
@@ -160,6 +161,40 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <inheritdoc/>
+        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromVisualTree(e);
+
+            if (IsDefault)
+            {
+                if (e.Root is IInputElement inputElement)
+                {
+                    StopListeningForDefault(inputElement);
+                }
+            }
+        }
+
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged += CanExecuteChanged;
+            }
+        }
+
+        protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged -= CanExecuteChanged;
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         {
@@ -195,20 +230,6 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <inheritdoc/>
-        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
-        {
-            base.OnDetachedFromVisualTree(e);
-
-            if (IsDefault)
-            {
-                if (e.Root is IInputElement inputElement)
-                {
-                    StopListeningForDefault(inputElement);
-                }
-            }
-        }
-
         /// <summary>
         /// Invokes the <see cref="Click"/> event.
         /// </summary>
@@ -281,17 +302,17 @@ namespace Avalonia.Controls
         {
             if (e.Sender is Button button)
             {
-                var oldCommand = e.OldValue as ICommand;
-                var newCommand = e.NewValue as ICommand;
-
-                if (oldCommand != null)
-                {
-                    oldCommand.CanExecuteChanged -= button.CanExecuteChanged;
-                }
-
-                if (newCommand != null)
+                if (((ILogical)button).IsAttachedToLogicalTree)
                 {
-                    newCommand.CanExecuteChanged += button.CanExecuteChanged;
+                    if (e.OldValue is ICommand oldCommand)
+                    {
+                        oldCommand.CanExecuteChanged -= button.CanExecuteChanged;
+                    }
+
+                    if (e.NewValue is ICommand newCommand)
+                    {
+                        newCommand.CanExecuteChanged += button.CanExecuteChanged;
+                    }
                 }
 
                 button.CanExecuteChanged(button, EventArgs.Empty);

+ 21 - 0
src/Avalonia.Controls/Grid.cs

@@ -177,6 +177,17 @@ namespace Avalonia.Controls
             return element.GetValue(RowSpanProperty);
         }
 
+
+        /// <summary>
+        /// Gets the value of the IsSharedSizeScope attached property for a control.
+        /// </summary>
+        /// <param name="element">The control.</param>
+        /// <returns>The control's IsSharedSizeScope value.</returns>
+        public static bool GetIsSharedSizeScope(AvaloniaObject element)
+        {
+            return element.GetValue(IsSharedSizeScopeProperty);
+        }
+
         /// <summary>
         /// Sets the value of the Column attached property for a control.
         /// </summary>
@@ -217,6 +228,16 @@ namespace Avalonia.Controls
             element.SetValue(RowSpanProperty, value);
         }
 
+        /// <summary>
+        /// Sets the value of IsSharedSizeScope property for a control.
+        /// </summary>
+        /// <param name="element">The control.</param>
+        /// <param name="value">The IsSharedSizeScope value.</param>
+        public static void SetIsSharedSizeScope(AvaloniaObject element, bool value)
+        {
+            element.SetValue(IsSharedSizeScopeProperty, value);
+        }
+
         /// <summary>
         /// Gets the result of the last column measurement.
         /// Use this result to reduce the arrange calculation.

+ 2 - 2
src/Avalonia.Controls/ListBox.cs

@@ -30,13 +30,13 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="SelectedItems"/> property.
         /// </summary>
-        public static readonly new AvaloniaProperty<IList> SelectedItemsProperty =
+        public static readonly new DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
             SelectingItemsControl.SelectedItemsProperty;
 
         /// <summary>
         /// Defines the <see cref="SelectionMode"/> property.
         /// </summary>
-        public static readonly new AvaloniaProperty<SelectionMode> SelectionModeProperty = 
+        public static readonly new StyledProperty<SelectionMode> SelectionModeProperty = 
             SelectingItemsControl.SelectionModeProperty;
 
         /// <summary>

+ 29 - 6
src/Avalonia.Controls/MenuItem.cs

@@ -286,6 +286,26 @@ namespace Avalonia.Controls
             return new MenuItemContainerGenerator(this);
         }
 
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged += CanExecuteChanged;
+            }
+        }
+
+        protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromLogicalTree(e);
+
+            if (Command != null)
+            {
+                Command.CanExecuteChanged -= CanExecuteChanged;
+            }
+        }
+
         /// <summary>
         /// Called when the <see cref="MenuItem"/> is clicked.
         /// </summary>
@@ -399,14 +419,17 @@ namespace Avalonia.Controls
         {
             if (e.Sender is MenuItem menuItem)
             {
-                if (e.OldValue is ICommand oldCommand)
+                if (((ILogical)menuItem).IsAttachedToLogicalTree)
                 {
-                    oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
-                }
+                    if (e.OldValue is ICommand oldCommand)
+                    {
+                        oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
+                    }
 
-                if (e.NewValue is ICommand newCommand)
-                {
-                    newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+                    if (e.NewValue is ICommand newCommand)
+                    {
+                        newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+                    }
                 }
 
                 menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);

+ 4 - 1
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@@ -147,7 +147,10 @@ namespace Avalonia.Platform
                 e.Handled = true;
             }
             else if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt)
-                RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers);
+            {
+                if (_lastRoot != null)
+                    RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers);
+            }
         }
 
         private void ProcessMouseEvents(RawMouseEventArgs e)

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

@@ -91,12 +91,12 @@ namespace Avalonia.Controls.Primitives
 
                 if (screenX > screen.Bounds.Width)
                 {
-                    Position = Position.WithX(Position.X - screenX - bounds.Width);
+                    Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width));
                 }
 
                 if (screenY > screen.Bounds.Height)
                 {
-                    Position = Position.WithY(Position.Y - screenY - bounds.Height);
+                    Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height));
                 }
             }
         }

+ 42 - 30
src/Avalonia.Controls/Shapes/Shape.cs

@@ -21,13 +21,19 @@ namespace Avalonia.Controls.Shapes
 
         public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty =
             AvaloniaProperty.Register<Shape, AvaloniaList<double>>(nameof(StrokeDashArray));
-            
+
         public static readonly StyledProperty<double> StrokeDashOffsetProperty =
             AvaloniaProperty.Register<Shape, double>(nameof(StrokeDashOffset));
 
         public static readonly StyledProperty<double> StrokeThicknessProperty =
             AvaloniaProperty.Register<Shape, double>(nameof(StrokeThickness));
 
+        public static readonly StyledProperty<PenLineCap> StrokeLineCapProperty =
+            AvaloniaProperty.Register<Shape, PenLineCap>(nameof(StrokeLineCap), PenLineCap.Flat);
+
+        public static readonly StyledProperty<PenLineJoin> StrokeJoinProperty =
+            AvaloniaProperty.Register<Shape, PenLineJoin>(nameof(StrokeJoin), PenLineJoin.Miter);
+
         private Matrix _transform = Matrix.Identity;
         private Geometry _definingGeometry;
         private Geometry _renderedGeometry;
@@ -36,7 +42,9 @@ namespace Avalonia.Controls.Shapes
         static Shape()
         {
             AffectsMeasure<Shape>(StretchProperty, StrokeThicknessProperty);
-            AffectsRender<Shape>(FillProperty, StrokeProperty, StrokeDashArrayProperty);
+
+            AffectsRender<Shape>(FillProperty, StrokeProperty, StrokeDashArrayProperty, StrokeDashOffsetProperty,
+                StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty);
         }
 
         public Geometry DefiningGeometry
@@ -106,7 +114,7 @@ namespace Avalonia.Controls.Shapes
             get { return GetValue(StrokeDashArrayProperty); }
             set { SetValue(StrokeDashArrayProperty, value); }
         }
-        
+
         public double StrokeDashOffset
         {
             get { return GetValue(StrokeDashOffsetProperty); }
@@ -119,13 +127,17 @@ namespace Avalonia.Controls.Shapes
             set { SetValue(StrokeThicknessProperty, value); }
         }
 
-        public PenLineCap StrokeDashCap { get; set; } = PenLineCap.Flat;
-
-        public PenLineCap StrokeStartLineCap { get; set; } = PenLineCap.Flat;
-
-        public PenLineCap StrokeEndLineCap { get; set; } = PenLineCap.Flat;
+        public PenLineCap StrokeLineCap
+        {
+            get { return GetValue(StrokeLineCapProperty); }
+            set { SetValue(StrokeLineCapProperty, value); }
+        }
 
-        public PenLineJoin StrokeJoin { get; set; } = PenLineJoin.Miter;
+        public PenLineJoin StrokeJoin
+        {
+            get { return GetValue(StrokeJoinProperty); }
+            set { SetValue(StrokeJoinProperty, value); }
+        }
 
         public override void Render(DrawingContext context)
         {
@@ -133,8 +145,8 @@ namespace Avalonia.Controls.Shapes
 
             if (geometry != null)
             {
-                var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), 
-                    StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin);
+                var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset),
+                     StrokeLineCap, StrokeJoin);
                 context.DrawGeometry(Fill, pen, geometry);
             }
         }
@@ -169,11 +181,11 @@ namespace Avalonia.Controls.Shapes
 
         protected void InvalidateGeometry()
         {
-            this._renderedGeometry = null;
-            this._definingGeometry = null;
+            _renderedGeometry = null;
+            _definingGeometry = null;
             InvalidateMeasure();
         }
-        
+
         protected override Size MeasureOverride(Size availableSize)
         {
             bool deferCalculateTransform;
@@ -203,10 +215,10 @@ namespace Avalonia.Controls.Shapes
                 return CalculateShapeSizeAndSetTransform(availableSize);
             }
         }
-        
+
         protected override Size ArrangeOverride(Size finalSize)
         {
-            if(_calculateTransformOnArrange)
+            if (_calculateTransformOnArrange)
             {
                 _calculateTransformOnArrange = false;
                 CalculateShapeSizeAndSetTransform(finalSize);
@@ -312,25 +324,25 @@ namespace Avalonia.Controls.Shapes
 
         private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)
         {
-            var control = e.Sender as Shape;
+            if (!(e.Sender is Shape control))
+            {
+                return;
+            }
 
-            if (control != null)
+            // If the geometry is invalidated when Bounds changes, only invalidate when the Size
+            // portion changes.
+            if (e.Property == BoundsProperty)
             {
-                // If the geometry is invalidated when Bounds changes, only invalidate when the Size
-                // portion changes.
-                if (e.Property == BoundsProperty)
-                {
-                    var oldBounds = (Rect)e.OldValue;
-                    var newBounds = (Rect)e.NewValue;
+                var oldBounds = (Rect)e.OldValue;
+                var newBounds = (Rect)e.NewValue;
 
-                    if (oldBounds.Size == newBounds.Size)
-                    {
-                        return;
-                    }
+                if (oldBounds.Size == newBounds.Size)
+                {
+                    return;
                 }
-
-                control.InvalidateGeometry();
             }
+
+            control.InvalidateGeometry();
         }
     }
 }

+ 3 - 5
src/Avalonia.Controls/TreeView.cs

@@ -40,17 +40,15 @@ namespace Avalonia.Controls
         /// Defines the <see cref="SelectedItems"/> property.
         /// </summary>
         public static readonly DirectProperty<TreeView, IList> SelectedItemsProperty =
-            AvaloniaProperty.RegisterDirect<TreeView, IList>(
-                nameof(SelectedItems),
+            ListBox.SelectedItemsProperty.AddOwner<TreeView>(
                 o => o.SelectedItems,
                 (o, v) => o.SelectedItems = v);
 
         /// <summary>
         /// Defines the <see cref="SelectionMode"/> property.
         /// </summary>
-        protected static readonly StyledProperty<SelectionMode> SelectionModeProperty =
-            AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
-                nameof(SelectionMode));
+        public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
+            ListBox.SelectionModeProperty.AddOwner<TreeView>();
 
         private static readonly IList Empty = new object[0];
         private object _selectedItem;

+ 13 - 1
src/Avalonia.Controls/Window.cs

@@ -291,7 +291,8 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="dialogResult">The dialog result.</param>
         /// <remarks>
-        /// When the window is shown with the <see cref="ShowDialog{TResult}"/> method, the
+        /// When the window is shown with the <see cref="ShowDialog{TResult}(IWindowImpl)"/>
+        /// or <see cref="ShowDialog{TResult}(Window)"/> method, the
         /// resulting task will produce the <see cref="_dialogResult"/> value when the window
         /// is closed.
         /// </remarks>
@@ -370,8 +371,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Shows the window.
         /// </summary>
+        /// <exception cref="InvalidOperationException">
+        /// The window has already been closed.
+        /// </exception>
         public override void Show()
         {
+            if (PlatformImpl == null)
+            {
+                throw new InvalidOperationException("Cannot re-show a closed window.");
+            }
+
             if (IsVisible)
             {
                 return;
@@ -396,6 +405,9 @@ namespace Avalonia.Controls
         /// Shows the window as a dialog.
         /// </summary>
         /// <param name="owner">The dialog's owner window.</param>
+        /// <exception cref="InvalidOperationException">
+        /// The window has already been closed.
+        /// </exception>
         /// <returns>
         /// A task that can be used to track the lifetime of the dialog.
         /// </returns>

+ 6 - 1
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@@ -6,10 +6,15 @@ using Avalonia.Threading;
 using ReactiveUI;
 using Splat;
 
-namespace Avalonia
+namespace Avalonia.ReactiveUI
 {
     public static class AppBuilderExtensions
     {
+        /// <summary>
+        /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia 
+        /// scheduler and Avalonia activation for view fetcher. Always remember to
+        /// call this method if you are using ReactiveUI in your application.
+        /// </summary>
         public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
             where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
         {

+ 8 - 0
src/Avalonia.ReactiveUI/Attributes.cs

@@ -0,0 +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 System.Reflection;
+using System.Runtime.CompilerServices;
+using Avalonia.Metadata;
+
+[assembly: XmlnsDefinition("http://reactiveui.net", "Avalonia.ReactiveUI")]

+ 17 - 1
src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs

@@ -9,15 +9,24 @@ using Avalonia.VisualTree;
 using Avalonia.Controls;
 using ReactiveUI;
 
-namespace Avalonia
+namespace Avalonia.ReactiveUI
 {
+    /// <summary>
+    /// Determines when Avalonia IVisuals get activated.
+    /// </summary>
     public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher
     {
+        /// <summary>
+        /// Returns affinity for view.
+        /// </summary>
         public int GetAffinityForView(Type view)
         {
             return typeof(IVisual).GetTypeInfo().IsAssignableFrom(view.GetTypeInfo()) ? 10 : 0;
         }
 
+        /// <summary>
+        /// Returns activation observable for activatable Avalonia view.
+        /// </summary>
         public IObservable<bool> GetActivationForView(IActivatable view)
         {
             if (!(view is IVisual visual)) return Observable.Return(false);
@@ -25,6 +34,9 @@ namespace Avalonia
             return GetActivationForVisual(visual);
         }
 
+        /// <summary>
+        /// Listens to Opened and Closed events for Avalonia windows.
+        /// </summary>
         private IObservable<bool> GetActivationForWindowBase(WindowBase window) 
         {
             var windowLoaded = Observable
@@ -42,6 +54,10 @@ namespace Avalonia
                 .DistinctUntilChanged();
         }
 
+        /// <summary>
+        /// Listens to AttachedToVisualTree and DetachedFromVisualTree 
+        /// events for Avalonia IVisuals.
+        /// </summary>
         private IObservable<bool> GetActivationForVisual(IVisual visual) 
         {
             var visualLoaded = Observable

+ 1 - 1
src/Avalonia.ReactiveUI/ReactiveUserControl.cs

@@ -6,7 +6,7 @@ using Avalonia.VisualTree;
 using Avalonia.Controls;
 using ReactiveUI;
 
-namespace Avalonia
+namespace Avalonia.ReactiveUI
 {
     /// <summary>
     /// A ReactiveUI UserControl that implements <see cref="IViewFor{TViewModel}"/> 

+ 1 - 1
src/Avalonia.ReactiveUI/ReactiveWindow.cs

@@ -6,7 +6,7 @@ using Avalonia.VisualTree;
 using Avalonia.Controls;
 using ReactiveUI;
 
-namespace Avalonia 
+namespace Avalonia.ReactiveUI
 {
     /// <summary>
     /// A ReactiveUI Window that implements <see cref="IViewFor{TViewModel}"/>

+ 6 - 2
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@@ -1,13 +1,17 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
 using System;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
 using Avalonia.Animation;
 using Avalonia.Controls;
 using Avalonia.Styling;
+using Avalonia;
 using ReactiveUI;
 using Splat;
 
-namespace Avalonia
+namespace Avalonia.ReactiveUI
 {
     /// <summary>
     /// This control hosts the View associated with ReactiveUI RoutingState,
@@ -157,7 +161,7 @@ namespace Avalonia
                 return;
             }
     
-            var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
+            var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
             var view = viewLocator.ResolveView(viewModel);
             if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?");
     

+ 1 - 0
src/Avalonia.Styling/StyledElement.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.ComponentModel;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Reactive.Subjects;

+ 12 - 14
src/Avalonia.Themes.Default/ScrollBar.xaml

@@ -30,13 +30,7 @@
                                               Classes="repeattrack"
                                               Focusable="False"/>
                             </Track.IncreaseButton>
-                            <Thumb Name="thumb">
-                                <Thumb.Template>
-                                    <ControlTemplate>
-                                        <Border Background="{DynamicResource ThemeControlHighBrush}" />
-                                    </ControlTemplate>
-                                </Thumb.Template>
-                            </Thumb>
+                            <Thumb Name="thumb"/>
                         </Track>
                         <RepeatButton Name="PART_LineDownButton"
                                       Classes="repeat"
@@ -84,13 +78,7 @@
                                               Classes="repeattrack"
                                               Focusable="False"/>
                             </Track.IncreaseButton>
-                            <Thumb Name="thumb">
-                                <Thumb.Template>
-                                    <ControlTemplate>
-                                        <Border Background="{DynamicResource ThemeControlHighBrush}" />
-                                    </ControlTemplate>
-                                </Thumb.Template>
-                            </Thumb>
+                            <Thumb Name="thumb"/>
                         </Track>
                         <RepeatButton Name="PART_LineDownButton"
                                       Classes="repeat"
@@ -106,6 +94,16 @@
             </ControlTemplate>
         </Setter>
     </Style>
+    <Style Selector="ScrollBar /template/ Thumb#thumb">
+        <Setter Property="Background" Value="{DynamicResource ThemeControlHighBrush}"/>
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate>
+                    <Border Background="{TemplateBinding Background}"/>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
     <Style Selector="ScrollBar:horizontal /template/ Thumb#thumb">
         <Setter Property="MinWidth" Value="{DynamicResource ScrollBarThickness}" />
     </Style>

+ 4 - 6
src/Avalonia.Visuals/Media/BrushExtensions.cs

@@ -34,16 +34,14 @@ namespace Avalonia.Media
         {
             Contract.Requires<ArgumentNullException>(pen != null);
 
-            var brush = pen?.Brush?.ToImmutable();
-            return pen == null || ReferenceEquals(pen?.Brush, brush) ?
+            var brush = pen.Brush?.ToImmutable();
+            return ReferenceEquals(pen.Brush, brush) ?
                 pen :
                 new Pen(
                     brush,
                     thickness: pen.Thickness,
-                    dashStyle: pen.DashStyle,
-                    dashCap: pen.DashCap,
-                    startLineCap: pen.StartLineCap,
-                    endLineCap: pen.EndLineCap,
+                    dashStyle: pen.DashStyle,                   
+                    lineCap: pen.LineCap,
                     lineJoin: pen.LineJoin,
                     miterLimit: pen.MiterLimit);
         }

+ 7 - 0
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
 using Avalonia.Threading;
 using Avalonia.Visuals.Media.Imaging;
 
@@ -131,6 +132,12 @@ namespace Avalonia.Media
             }
         }
 
+        /// <summary>
+        /// Draws a custom drawing operation
+        /// </summary>
+        /// <param name="custom">custom operation</param>
+        public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom);
+
         /// <summary>
         /// Draws text.
         /// </summary>

+ 1 - 30
src/Avalonia.Visuals/Media/EllipseGeometry.cs

@@ -1,7 +1,6 @@
 // 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.Platform;
 
 namespace Avalonia.Media
@@ -57,36 +56,8 @@ namespace Avalonia.Media
         protected override IGeometryImpl CreateDefiningGeometry()
         {
             var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            var geometry = factory.CreateStreamGeometry();
 
-            using (var ctx = geometry.Open())
-            {
-                var rect = Rect;
-                double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3;
-                var center = rect.Center;
-                var radius = new Vector(rect.Width / 2, rect.Height / 2);
-
-                var x0 = center.X - radius.X;
-                var x1 = center.X - (radius.X * controlPointRatio);
-                var x2 = center.X;
-                var x3 = center.X + (radius.X * controlPointRatio);
-                var x4 = center.X + radius.X;
-
-                var y0 = center.Y - radius.Y;
-                var y1 = center.Y - (radius.Y * controlPointRatio);
-                var y2 = center.Y;
-                var y3 = center.Y + (radius.Y * controlPointRatio);
-                var y4 = center.Y + radius.Y;
-
-                ctx.BeginFigure(new Point(x2, y0), true);
-                ctx.CubicBezierTo(new Point(x3, y0), new Point(x4, y1), new Point(x4, y2));
-                ctx.CubicBezierTo(new Point(x4, y3), new Point(x3, y4), new Point(x2, y4));
-                ctx.CubicBezierTo(new Point(x1, y4), new Point(x0, y3), new Point(x0, y2));
-                ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0));
-                ctx.EndFigure(true);
-            }
-
-            return geometry;
+            return factory.CreateEllipseGeometry(Rect);
         }
     }
 }

+ 1 - 9
src/Avalonia.Visuals/Media/LineGeometry.cs

@@ -73,16 +73,8 @@ namespace Avalonia.Media
         protected override IGeometryImpl CreateDefiningGeometry()
         {
             var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            var geometry = factory.CreateStreamGeometry();
 
-            using (var context = geometry.Open())
-            {
-                context.BeginFigure(StartPoint, false);
-                context.LineTo(EndPoint);
-                context.EndFigure(false);
-            }
-
-            return geometry;
+            return factory.CreateLineGeometry(StartPoint, EndPoint);
         }
     }
 }

+ 31 - 41
src/Avalonia.Visuals/Media/Pen.cs

@@ -11,63 +11,45 @@ namespace Avalonia.Media
         /// <summary>
         /// Initializes a new instance of the <see cref="Pen"/> class.
         /// </summary>
-        /// <param name="brush">The brush used to draw.</param>
+        /// <param name="color">The stroke color.</param>
         /// <param name="thickness">The stroke thickness.</param>
         /// <param name="dashStyle">The dash style.</param>
-        /// <param name="dashCap">The dash cap.</param>
-        /// <param name="startLineCap">The start line cap.</param>
-        /// <param name="endLineCap">The end line cap.</param>
+        /// <param name="lineCap">Specifies the type of graphic shape to use on both ends of a line.</param>
         /// <param name="lineJoin">The line join.</param>
         /// <param name="miterLimit">The miter limit.</param>
         public Pen(
-            IBrush brush, 
+            uint color,
             double thickness = 1.0,
-            DashStyle dashStyle = null, 
-            PenLineCap dashCap = PenLineCap.Flat, 
-            PenLineCap startLineCap = PenLineCap.Flat, 
-            PenLineCap endLineCap = PenLineCap.Flat, 
-            PenLineJoin lineJoin = PenLineJoin.Miter, 
-            double miterLimit = 10.0)
+            DashStyle dashStyle = null,
+            PenLineCap lineCap = PenLineCap.Flat,
+            PenLineJoin lineJoin = PenLineJoin.Miter,
+            double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
         {
-            Brush = brush;
-            Thickness = thickness;
-            DashCap = dashCap;
-            StartLineCap = startLineCap;
-            EndLineCap = endLineCap;
-            LineJoin = lineJoin;
-            MiterLimit = miterLimit;
-            DashStyle = dashStyle;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Pen"/> class.
         /// </summary>
-        /// <param name="color">The stroke color.</param>
+        /// <param name="brush">The brush used to draw.</param>
         /// <param name="thickness">The stroke thickness.</param>
         /// <param name="dashStyle">The dash style.</param>
-        /// <param name="dashCap">The dash cap.</param>
-        /// <param name="startLineCap">The start line cap.</param>
-        /// <param name="endLineCap">The end line cap.</param>
+        /// <param name="lineCap">The line cap.</param>
         /// <param name="lineJoin">The line join.</param>
         /// <param name="miterLimit">The miter limit.</param>
         public Pen(
-            uint color, 
+            IBrush brush,
             double thickness = 1.0,
-            DashStyle dashStyle = null, 
-            PenLineCap dashCap = PenLineCap.Flat, 
-            PenLineCap startLineCap = PenLineCap.Flat,
-            PenLineCap endLineCap = PenLineCap.Flat, 
-            PenLineJoin lineJoin = PenLineJoin.Miter, 
+            DashStyle dashStyle = null,
+            PenLineCap lineCap = PenLineCap.Flat,
+            PenLineJoin lineJoin = PenLineJoin.Miter,
             double miterLimit = 10.0)
         {
-            Brush = new SolidColorBrush(color);
+            Brush = brush;
             Thickness = thickness;
-            StartLineCap = startLineCap;
-            EndLineCap = endLineCap;
+            LineCap = lineCap;
             LineJoin = lineJoin;
             MiterLimit = miterLimit;
             DashStyle = dashStyle;
-            DashCap = dashCap;
         }
 
         /// <summary>
@@ -78,18 +60,26 @@ namespace Avalonia.Media
         /// <summary>
         /// Gets the stroke thickness.
         /// </summary>
-        public double Thickness { get; } = 1.0;
+        public double Thickness { get; }
 
+        /// <summary>
+        /// Specifies the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
         public DashStyle DashStyle { get; }
 
-        public PenLineCap DashCap { get; }
-
-        public PenLineCap StartLineCap { get; } = PenLineCap.Flat;
-
-        public PenLineCap EndLineCap { get; } = PenLineCap.Flat;
+        /// <summary>
+        /// Specifies the type of graphic shape to use on both ends of a line.
+        /// </summary>
+        public PenLineCap LineCap { get; }
 
-        public PenLineJoin LineJoin { get; } = PenLineJoin.Miter;
+        /// <summary>
+        /// Specifies how to join consecutive line or curve segments in a <see cref="PathFigure"/> (subpath) contained in a <see cref="PathGeometry"/> object.
+        /// </summary>
+        public PenLineJoin LineJoin { get; }
 
-        public double MiterLimit { get; } = 10.0;
+        /// <summary>
+        /// The limit on the ratio of the miter length to half this pen's Thickness.
+        /// </summary>
+        public double MiterLimit { get; }
     }
 }

+ 1 - 2
src/Avalonia.Visuals/Media/PenLineCap.cs

@@ -4,7 +4,6 @@ namespace Avalonia.Media
     {
         Flat,
         Round,
-        Square,
-        Triangle
+        Square
     }
 }

+ 10 - 18
src/Avalonia.Visuals/Media/RectangleGeometry.cs

@@ -16,12 +16,6 @@ namespace Avalonia.Media
         public static readonly StyledProperty<Rect> RectProperty =
             AvaloniaProperty.Register<RectangleGeometry, Rect>(nameof(Rect));
 
-        public Rect Rect
-        {
-            get => GetValue(RectProperty);
-            set => SetValue(RectProperty, value);
-        }
-
         static RectangleGeometry()
         {
             AffectsGeometry(RectProperty);
@@ -43,25 +37,23 @@ namespace Avalonia.Media
             Rect = rect;
         }
 
+        /// <summary>
+        /// Gets or sets the bounds of the rectangle.
+        /// </summary>
+        public Rect Rect
+        {
+            get => GetValue(RectProperty);
+            set => SetValue(RectProperty, value);
+        }
+
         /// <inheritdoc/>
         public override Geometry Clone() => new RectangleGeometry(Rect);
 
         protected override IGeometryImpl CreateDefiningGeometry()
         {
             var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            var geometry = factory.CreateStreamGeometry();
-
-            using (var context = geometry.Open())
-            {
-                var rect = Rect;
-                context.BeginFigure(rect.TopLeft, true);
-                context.LineTo(rect.TopRight);
-                context.LineTo(rect.BottomRight);
-                context.LineTo(rect.BottomLeft);
-                context.EndFigure(true);
-            }
 
-            return geometry;
+            return factory.CreateRectangleGeometry(Rect);
         }
     }
 }

+ 7 - 0
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Media;
+using Avalonia.Rendering.SceneGraph;
 using Avalonia.Utilities;
 using Avalonia.Visuals.Media.Imaging;
 
@@ -139,5 +140,11 @@ namespace Avalonia.Platform
         /// Pops the latest pushed geometry clip.
         /// </summary>
         void PopGeometryClip();
+
+        /// <summary>
+        /// Adds a custom draw operation
+        /// </summary>
+        /// <param name="custom">Custom draw operation</param>
+        void Custom(ICustomDrawOperation custom);
     }
 }

+ 22 - 0
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@@ -36,6 +36,28 @@ namespace Avalonia.Platform
             Size constraint,
             IReadOnlyList<FormattedTextStyleSpan> spans);
 
+        /// <summary>
+        /// Creates an ellipse geometry implementation.
+        /// </summary>
+        /// <param name="rect">The bounds of the ellipse.</param>
+        /// <returns>An ellipse geometry..</returns>
+        IGeometryImpl CreateEllipseGeometry(Rect rect);
+
+        /// <summary>
+        /// Creates a line geometry implementation.
+        /// </summary>
+        /// <param name="p1">The start of the line.</param>
+        /// <param name="p2">The end of the line.</param>
+        /// <returns>A line geometry.</returns>
+        IGeometryImpl CreateLineGeometry(Point p1, Point p2);
+
+        /// <summary>
+        /// Creates a rectangle geometry implementation.
+        /// </summary>
+        /// <param name="rect">The bounds of the rectangle.</param>
+        /// <returns>A rectangle.</returns>
+        IGeometryImpl CreateRectangleGeometry(Rect rect);
+
         /// <summary>
         /// Creates a stream geometry implementation.
         /// </summary>

+ 39 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs

@@ -0,0 +1,39 @@
+using System;
+using Avalonia.Media;
+using Avalonia.Platform;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    internal sealed class CustomDrawOperation : DrawOperation
+    {
+        public Matrix Transform { get; }
+        public ICustomDrawOperation Custom { get; }
+        public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) 
+            : base(custom.Bounds, transform, null)
+        {
+            Transform = transform;
+            Custom = custom;
+        }
+
+        public override bool HitTest(Point p)
+        {
+            return Custom.HitTest(p * Transform);
+        }
+
+        public override void Render(IDrawingContextImpl context)
+        {
+            context.Transform = Transform;
+            Custom.Render(context);
+        }
+
+        public override void Dispose() => Custom.Dispose();
+
+        public bool Equals(Matrix transform, ICustomDrawOperation custom) =>
+            Transform == transform && Custom?.Equals(custom) == true;
+    }
+
+    public interface ICustomDrawOperation : IDrawOperation, IEquatable<ICustomDrawOperation>
+    {
+        
+    }
+}

+ 9 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -165,6 +165,15 @@ namespace Avalonia.Rendering.SceneGraph
                 ++_drawOperationindex;
             }
         }
+        
+        public void Custom(ICustomDrawOperation custom)
+        {
+            var next = NextDrawAs<CustomDrawOperation>();
+            if (next == null || !next.Item.Equals(Transform, custom))
+                Add(new CustomDrawOperation(custom, Transform));
+            else
+                ++_drawOperationindex;
+        }
 
         /// <inheritdoc/>
         public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

@@ -162,7 +162,8 @@ namespace Avalonia.Markup.Xaml
             var readerSettings = new XamlXmlReaderSettings()
             {
                 BaseUri = uri,
-                LocalAssembly = localAssembly
+                LocalAssembly = localAssembly,
+                ProvideLineInfo = true,
             };
 
             var context = IsDesignMode ? AvaloniaXamlSchemaContext.DesignInstance : AvaloniaXamlSchemaContext.Instance;

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github

@@ -1 +1 @@
-Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975
+Subproject commit 7452b23169e4948907fa10e2c115b672897d0e04

+ 42 - 0
src/Skia/Avalonia.Skia/CustomRenderTarget.cs

@@ -0,0 +1,42 @@
+// 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.Platform;
+using Avalonia.Rendering;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Adapts <see cref="ICustomSkiaRenderTarget"/> to be used within Skia rendering pipeline.
+    /// </summary>
+    internal class CustomRenderTarget : IRenderTarget
+    {
+        private readonly ICustomSkiaRenderTarget _renderTarget;
+
+        public CustomRenderTarget(ICustomSkiaRenderTarget renderTarget)
+        {
+            _renderTarget = renderTarget;
+        }
+
+        public void Dispose()
+        {
+            _renderTarget.Dispose();
+        }
+
+        public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
+        {
+            ICustomSkiaRenderSession session = _renderTarget.BeginRendering();
+
+            var nfo = new DrawingContextImpl.CreateInfo
+            {
+                GrContext = session.GrContext,
+                Canvas = session.Canvas,
+                Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor,
+                VisualBrushRenderer = visualBrushRenderer,
+                DisableTextLcdRendering = true
+            };
+
+            return new DrawingContextImpl(nfo, session);
+        }
+    }
+}

+ 11 - 15
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -9,6 +9,7 @@ using System.Threading;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Rendering.SceneGraph;
 using Avalonia.Rendering.Utilities;
 using Avalonia.Utilities;
 using Avalonia.Visuals.Media.Imaging;
@@ -19,7 +20,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia based drawing context.
     /// </summary>
-    public class DrawingContextImpl : IDrawingContextImpl
+    internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl
     {
         private IDisposable[] _disposables;
         private readonly Vector _dpi;
@@ -99,6 +100,8 @@ namespace Avalonia.Skia
         /// </summary>
         public SKCanvas Canvas { get; }
 
+        SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas;
+
         /// <inheritdoc />
         public void Clear(Color color)
         {
@@ -296,6 +299,8 @@ namespace Avalonia.Skia
             Canvas.Restore();
         }
 
+        public void Custom(ICustomDrawOperation custom) => custom.Render(this);
+
         /// <inheritdoc />
         public void PushOpacityMask(IBrush mask, Rect bounds)
         {
@@ -573,25 +578,17 @@ namespace Avalonia.Skia
             // Need to modify dashes due to Skia modifying their lengths
             // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
             // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
-            float dashLengthModifier;
-            float gapLengthModifier;
 
-            switch (pen.StartLineCap)
+            switch (pen.LineCap)
             {
                 case PenLineCap.Round:
                     paint.StrokeCap = SKStrokeCap.Round;
-                    dashLengthModifier = -paint.StrokeWidth;
-                    gapLengthModifier = paint.StrokeWidth;
                     break;
                 case PenLineCap.Square:
                     paint.StrokeCap = SKStrokeCap.Square;
-                    dashLengthModifier = -paint.StrokeWidth;
-                    gapLengthModifier = paint.StrokeWidth;
                     break;
                 default:
                     paint.StrokeCap = SKStrokeCap.Butt;
-                    dashLengthModifier = 0.0f;
-                    gapLengthModifier = 0.0f;
                     break;
             }
 
@@ -617,13 +614,12 @@ namespace Avalonia.Skia
 
                 for (var i = 0; i < srcDashes.Count; ++i)
                 {
-                    var lengthModifier = i % 2 == 0 ? dashLengthModifier : gapLengthModifier;
-
-                    // Avalonia dash lengths are relative, but Skia takes absolute sizes - need to scale
-                    dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth + lengthModifier;
+                    dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth;
                 }
 
-                var pe = SKPathEffect.CreateDash(dashesArray, (float) pen.DashStyle.Offset);
+                var offset = (float)(pen.DashStyle.Offset * pen.Thickness);
+
+                var pe = SKPathEffect.CreateDash(dashesArray, offset);
 
                 paint.PathEffect = pe;
                 rv.AddDisposable(pe);

+ 25 - 0
src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs

@@ -0,0 +1,25 @@
+// 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 SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// A Skia implementation of a <see cref="Avalonia.Media.EllipseGeometry"/>.
+    /// </summary>
+    internal class EllipseGeometryImpl : GeometryImpl
+    {
+        public override Rect Bounds { get; }
+        public override SKPath EffectivePath { get; }
+
+        public EllipseGeometryImpl(Rect rect)
+        {
+            var path = new SKPath();
+            path.AddOval(rect.ToSKRect());
+
+            EffectivePath = path;
+            Bounds = rect;
+        }
+    }
+}

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

@@ -13,7 +13,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia formatted text implementation.
     /// </summary>
-    public class FormattedTextImpl : IFormattedTextImpl
+    internal class FormattedTextImpl : IFormattedTextImpl
     {
         public FormattedTextImpl(
             string text,

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

@@ -13,7 +13,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
     /// </summary>
-    public class FramebufferRenderTarget : IRenderTarget
+    internal class FramebufferRenderTarget : IRenderTarget
     {
         private readonly IFramebufferPlatformSurface _platformSurface;
         private SKImageInfo _currentImageInfo;

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

@@ -11,7 +11,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// A Skia implementation of <see cref="IGeometryImpl"/>.
     /// </summary>
-    public abstract class GeometryImpl : IGeometryImpl
+    internal abstract class GeometryImpl : IGeometryImpl
     {
         private PathCache _pathCache;
         

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

@@ -8,7 +8,7 @@ using static Avalonia.OpenGL.GlConsts;
 
 namespace Avalonia.Skia
 {
-    public class GlRenderTarget : IRenderTarget
+    internal class GlRenderTarget : IRenderTarget
     {
         private readonly GRContext _grContext;
         private IGlPlatformSurfaceRenderTarget _surface;

+ 26 - 0
src/Skia/Avalonia.Skia/ICustomSkiaGpu.cs

@@ -0,0 +1,26 @@
+// 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.Collections.Generic;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Custom Skia gpu instance.
+    /// </summary>
+    public interface ICustomSkiaGpu
+    {
+        /// <summary>
+        /// Skia GrContext used.
+        /// </summary>
+        GRContext GrContext { get; }
+
+        /// <summary>
+        /// Attempts to create custom render target from given surfaces.
+        /// </summary>
+        /// <param name="surfaces">Surfaces.</param>
+        /// <returns>Created render target or <see langword="null"/> if it fails.</returns>
+        ICustomSkiaRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces);
+    }
+}

+ 29 - 0
src/Skia/Avalonia.Skia/ICustomSkiaRenderSession.cs

@@ -0,0 +1,29 @@
+// 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 SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Custom render session for Skia render target.
+    /// </summary>
+    public interface ICustomSkiaRenderSession : IDisposable
+    {
+        /// <summary>
+        /// GrContext used by this session.
+        /// </summary>
+        GRContext GrContext { get; }
+
+        /// <summary>
+        /// Canvas that will be used to render.
+        /// </summary>
+        SKCanvas Canvas { get; }
+
+        /// <summary>
+        /// Scaling factor.
+        /// </summary>
+        double ScaleFactor { get; }
+    }
+}

+ 19 - 0
src/Skia/Avalonia.Skia/ICustomSkiaRenderTarget.cs

@@ -0,0 +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;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// Custom Skia render target.
+    /// </summary>
+    public interface ICustomSkiaRenderTarget : IDisposable
+    {
+        /// <summary>
+        /// Start rendering to this render target.
+        /// </summary>
+        /// <returns></returns>
+        ICustomSkiaRenderSession BeginRendering();
+    }
+}

+ 10 - 0
src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs

@@ -0,0 +1,10 @@
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    public interface ISkiaDrawingContextImpl : IDrawingContextImpl
+    {
+        SKCanvas SkCanvas { get; }
+    }
+}

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

@@ -12,7 +12,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Immutable Skia bitmap.
     /// </summary>
-    public class ImmutableBitmap : IDrawableBitmapImpl
+    internal class ImmutableBitmap : IDrawableBitmapImpl
     {
         private readonly SKImage _image;
 

+ 29 - 0
src/Skia/Avalonia.Skia/LineGeometryImpl.cs

@@ -0,0 +1,29 @@
+// 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 SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// A Skia implementation of a <see cref="Avalonia.Media.LineGeometry"/>.
+    /// </summary>
+    internal class LineGeometryImpl : GeometryImpl
+    {
+        public override Rect Bounds { get; }
+        public override SKPath EffectivePath { get; }
+
+        public LineGeometryImpl(Point p1, Point p2)
+        {
+            var path = new SKPath();
+            path.MoveTo(p1.ToSKPoint());
+            path.LineTo(p2.ToSKPoint());
+
+            EffectivePath = path;
+            Bounds = new Rect(
+                new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)), 
+                new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y)));
+        }
+    }
+}

+ 32 - 6
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -15,14 +15,25 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia platform render interface.
     /// </summary>
-    public class PlatformRenderInterface : IPlatformRenderInterface
+    internal class PlatformRenderInterface : IPlatformRenderInterface
     {
+        private readonly ICustomSkiaGpu _customSkiaGpu;
+
         private GRContext GrContext { get; }
 
         public IEnumerable<string> InstalledFontNames => SKFontManager.Default.FontFamilies;
 
-        public PlatformRenderInterface()
+        public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
         {
+            if (customSkiaGpu != null)
+            {
+                _customSkiaGpu = customSkiaGpu;
+
+                GrContext = _customSkiaGpu.GrContext;
+
+                return;
+            }
+
             var gl = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
             if (gl != null)
             {
@@ -32,12 +43,11 @@ namespace Avalonia.Skia
                     ? GRGlInterface.AssembleGlInterface((_, proc) => display.GlInterface.GetProcAddress(proc))
                     : GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc)))
                 {
-                    
                     GrContext = GRContext.Create(GRBackend.OpenGL, iface);
                 }
             }
         }
-        
+
         /// <inheritdoc />
         public IFormattedTextImpl CreateFormattedText(
             string text,
@@ -50,6 +60,12 @@ namespace Avalonia.Skia
             return new FormattedTextImpl(text, typeface, textAlignment, wrapping, constraint, spans);
         }
 
+        public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
+
+        public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);
+
+        public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect);
+
         /// <inheritdoc />
         public IStreamGeometryImpl CreateStreamGeometry()
         {
@@ -98,13 +114,23 @@ namespace Avalonia.Skia
                 DisableTextLcdRendering = false,
                 GrContext = GrContext
             };
-            
+
             return new SurfaceRenderTarget(createInfo);
         }
 
         /// <inheritdoc />
-        public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
+        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
+            if (_customSkiaGpu != null)
+            {
+                ICustomSkiaRenderTarget customRenderTarget = _customSkiaGpu.TryCreateRenderTarget(surfaces);
+
+                if (customRenderTarget != null)
+                {
+                    return new CustomRenderTarget(customRenderTarget);
+                }
+            }
+
             foreach (var surface in surfaces)
             {
                 if (surface is IGlPlatformSurface glSurface && GrContext != null)

+ 25 - 0
src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs

@@ -0,0 +1,25 @@
+// 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 SkiaSharp;
+
+namespace Avalonia.Skia
+{
+    /// <summary>
+    /// A Skia implementation of a <see cref="Avalonia.Media.RectangleGeometry"/>.
+    /// </summary>
+    internal class RectangleGeometryImpl : GeometryImpl
+    {
+        public override Rect Bounds { get; }
+        public override SKPath EffectivePath { get; }
+
+        public RectangleGeometryImpl(Rect rect)
+        {
+            var path = new SKPath();
+            path.AddRect(rect.ToSKRect());
+
+            EffectivePath = path;
+            Bounds = rect;
+        }
+    }
+}

+ 3 - 2
src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs

@@ -20,8 +20,9 @@ namespace Avalonia
         /// <returns>Configure builder.</returns>
         public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new()
         {
-            builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(), "Skia");
-            return builder;
+            return builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(
+                AvaloniaLocator.Current.GetService<SkiaOptions>() ?? new SkiaOptions()),
+                "Skia");
         }
     }
 }

+ 19 - 0
src/Skia/Avalonia.Skia/SkiaOptions.cs

@@ -0,0 +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;
+using Avalonia.Skia;
+
+namespace Avalonia
+{
+    /// <summary>
+    /// Options for Skia rendering subsystem.
+    /// </summary>
+    public class SkiaOptions
+    {
+        /// <summary>
+        /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer.
+        /// </summary>
+        public Func<ICustomSkiaGpu> CustomGpuFactory { get; set; }
+    }
+}

+ 8 - 2
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@@ -15,8 +15,14 @@ namespace Avalonia.Skia
         /// </summary>
         public static void Initialize()
         {
-            var renderInterface = new PlatformRenderInterface();
-            
+            Initialize(new SkiaOptions());
+        }
+
+        public static void Initialize(SkiaOptions options)
+        {
+            var customGpu = options.CustomGpuFactory?.Invoke();
+            var renderInterface = new PlatformRenderInterface(customGpu);
+
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformRenderInterface>().ToConstant(renderInterface);
         }

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

@@ -10,7 +10,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// A Skia implementation of a <see cref="IStreamGeometryImpl"/>.
     /// </summary>
-    public class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
+    internal class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
     {
         private Rect _bounds;
         private readonly SKPath _effectivePath;

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

@@ -14,7 +14,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia render target that writes to a surface.
     /// </summary>
-    public class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl
+    internal class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl
     {
         private readonly SKSurface _surface;
         private readonly SKCanvas _canvas;

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

@@ -9,7 +9,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// A Skia implementation of a <see cref="ITransformedGeometryImpl"/>.
     /// </summary>
-    public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
+    internal class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
     {
         /// <summary>
         ///  Initializes a new instance of the <see cref="TransformedGeometryImpl"/> class.

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

@@ -13,7 +13,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia based writeable bitmap.
     /// </summary>
-    public class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl
+    internal class WriteableBitmapImpl : IWriteableBitmapImpl, IDrawableBitmapImpl
     {
         private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc;
         private readonly SKBitmap _bitmap;

+ 4 - 4
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -182,10 +182,10 @@ namespace Avalonia.Direct2D1
             return new WriteableWicBitmapImpl(size, dpi, format);
         }
 
-        public IStreamGeometryImpl CreateStreamGeometry()
-        {
-            return new StreamGeometryImpl();
-        }
+        public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
+        public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);
+        public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect);
+        public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
 
         public IBitmapImpl LoadBitmap(string fileName)
         {

+ 3 - 0
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Rendering.SceneGraph;
 using Avalonia.Utilities;
 using SharpDX;
 using SharpDX.Direct2D1;
@@ -508,5 +509,7 @@ namespace Avalonia.Direct2D1.Media
         {
             PopLayer();
         }
+        
+        public void Custom(ICustomDrawOperation custom) => custom.Render(this);
     }
 }

+ 27 - 0
src/Windows/Avalonia.Direct2D1/Media/EllipseGeometryImpl.cs

@@ -0,0 +1,27 @@
+// 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 SharpDX.Direct2D1;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A Direct2D implementation of a <see cref="Avalonia.Media.EllipseGeometry"/>.
+    /// </summary>
+    internal class EllipseGeometryImpl : GeometryImpl
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        public EllipseGeometryImpl(Rect rect)
+            : base(CreateGeometry(rect))
+        {
+        }
+
+        private static Geometry CreateGeometry(Rect rect)
+        {
+            var ellipse = new Ellipse(rect.Center.ToSharpDX(), (float)rect.Width / 2, (float)rect.Height / 2);
+            return new EllipseGeometry(Direct2D1Platform.Direct2D1Factory, ellipse);
+        }
+    }
+}

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs

@@ -9,7 +9,7 @@ using DWrite = SharpDX.DirectWrite;
 
 namespace Avalonia.Direct2D1.Media
 {
-    public class FormattedTextImpl : IFormattedTextImpl
+    internal class FormattedTextImpl : IFormattedTextImpl
     {
         public FormattedTextImpl(
             string text,

+ 27 - 0
src/Windows/Avalonia.Direct2D1/Media/LineGeometryImpl.cs

@@ -0,0 +1,27 @@
+// 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 SharpDX.Direct2D1;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A Direct2D implementation of a <see cref="Avalonia.Media.LineGeometry"/>.
+    /// </summary>
+    internal class LineGeometryImpl : StreamGeometryImpl
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        public LineGeometryImpl(Point p1, Point p2)
+        {
+            using (var sink = ((PathGeometry)Geometry).Open())
+            {
+                sink.BeginFigure(p1.ToSharpDX(), FigureBegin.Hollow);
+                sink.AddLine(p2.ToSharpDX());
+                sink.EndFigure(FigureEnd.Open);
+                sink.Close();
+            }
+        }
+    }
+}

+ 26 - 0
src/Windows/Avalonia.Direct2D1/Media/RectangleGeometryImpl.cs

@@ -0,0 +1,26 @@
+// 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 SharpDX.Direct2D1;
+
+namespace Avalonia.Direct2D1.Media
+{
+    /// <summary>
+    /// A Direct2D implementation of a <see cref="Avalonia.Media.RectangleGeometry"/>.
+    /// </summary>
+    internal class RectangleGeometryImpl : GeometryImpl
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
+        /// </summary>
+        public RectangleGeometryImpl(Rect rect)
+            : base(CreateGeometry(rect))
+        {
+        }
+
+        private static Geometry CreateGeometry(Rect rect)
+        {
+            return new RectangleGeometry(Direct2D1Platform.Direct2D1Factory, rect.ToDirect2D());
+        }
+    }
+}

+ 5 - 3
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@@ -122,14 +122,16 @@ namespace Avalonia.Direct2D1
         /// <returns>The Direct2D brush.</returns>
         public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
         {
+            var d2dLineCap = pen.LineCap.ToDirect2D();
+
             var properties = new StrokeStyleProperties
             {
                 DashStyle = DashStyle.Solid,
                 MiterLimit = (float)pen.MiterLimit,
                 LineJoin = pen.LineJoin.ToDirect2D(),
-                StartCap = pen.StartLineCap.ToDirect2D(),
-                EndCap = pen.EndLineCap.ToDirect2D(),
-                DashCap = pen.DashCap.ToDirect2D()
+                StartCap = d2dLineCap,
+                EndCap = d2dLineCap,
+                DashCap = d2dLineCap
             };
             float[] dashes = null;
             if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)

+ 1 - 1
src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs

@@ -50,7 +50,7 @@ namespace Avalonia.Win32.Interop.Wpf
                 {
                     _resource = texture.QueryInterface<SharpDX.Direct3D11.Resource>();
                     
-                    Target = new RenderTarget(AvaloniaLocator.Current.GetService<SharpDX.Direct2D1.Factory>(), surface,
+                    Target = new RenderTarget(Direct2D1Platform.Direct2D1Factory, surface,
                         new RenderTargetProperties
                         {
                             DpiX = (float) dpi.X,

+ 44 - 2
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -5,6 +5,7 @@ using Avalonia.Input;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Moq;
 using Xunit;
@@ -21,6 +22,7 @@ namespace Avalonia.Controls.UnitTests
             {
                 Command = command,
             };
+            var root = new TestRoot { Child = target };
 
             Assert.False(target.IsEnabled);
             command.IsEnabled = true;
@@ -215,6 +217,39 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(clicked);
         }
 
+        [Fact]
+        public void Button_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand(true);
+            var target = new Button
+            {
+                Command = command,
+            };
+
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void Button_Subscribes_To_Command_CanExecuteChanged_When_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand(true);
+            var target = new Button { Command = command };
+            var root = new TestRoot { Child = target };
+
+            Assert.Equal(1, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void Button_Unsubscribes_From_Command_CanExecuteChanged_When_Removed_From_Logical_Tree()
+        {
+            var command = new TestCommand(true);
+            var target = new Button { Command = command };
+            var root = new TestRoot { Child = target };
+
+            root.Child = null;
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
         private class TestButton : Button, IRenderRoot
         {
             public TestButton()
@@ -298,6 +333,7 @@ namespace Avalonia.Controls.UnitTests
 
         private class TestCommand : ICommand
         {
+            private EventHandler _canExecuteChanged;
             private bool _enabled;
 
             public TestCommand(bool enabled)
@@ -313,12 +349,18 @@ namespace Avalonia.Controls.UnitTests
                     if (_enabled != value)
                     {
                         _enabled = value;
-                        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+                        _canExecuteChanged?.Invoke(this, EventArgs.Empty);
                     }
                 }
             }
 
-            public event EventHandler CanExecuteChanged;
+            public int SubscriptionCount { get; private set; }
+
+            public event EventHandler CanExecuteChanged
+            {
+                add { _canExecuteChanged += value; ++SubscriptionCount; }
+                remove { _canExecuteChanged -= value; --SubscriptionCount; }
+            }
 
             public bool CanExecute(object parameter) => _enabled;
 

+ 54 - 0
tests/Avalonia.Controls.UnitTests/MenuItemTests.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
+using System.Windows.Input;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -22,5 +24,57 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.False(target.Focusable);
         }
+
+        [Fact]
+        public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand();
+            var target = new MenuItem
+            {
+                Command = command,
+            };
+
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void MenuItem_Subscribes_To_Command_CanExecuteChanged_When_Added_To_Logical_Tree()
+        {
+            var command = new TestCommand();
+            var target = new MenuItem { Command = command };
+            var root = new TestRoot { Child = target };
+
+            Assert.Equal(1, command.SubscriptionCount);
+        }
+
+        [Fact]
+        public void MenuItem_Unsubscribes_From_Command_CanExecuteChanged_When_Removed_From_Logical_Tree()
+        {
+            var command = new TestCommand();
+            var target = new MenuItem { Command = command };
+            var root = new TestRoot { Child = target };
+
+            root.Child = null;
+            Assert.Equal(0, command.SubscriptionCount);
+        }
+
+        private class TestCommand : ICommand
+        {
+            private EventHandler _canExecuteChanged;
+
+            public int SubscriptionCount { get; private set; }
+
+            public event EventHandler CanExecuteChanged
+            {
+                add { _canExecuteChanged += value; ++SubscriptionCount; }
+                remove { _canExecuteChanged -= value; --SubscriptionCount; }
+            }
+
+            public bool CanExecute(object parameter) => true;
+
+            public void Execute(object parameter)
+            {
+            }
+        }
     }
 }

+ 45 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -292,6 +292,51 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Calling_Show_On_Closed_Window_Should_Throw()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windowImpl = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
+                var target = new Window(windowImpl);
+
+                target.Show();
+                target.Close();
+
+                var openedRaised = false;
+                target.Opened += (s, e) => openedRaised = true;
+
+                var ex = Assert.Throws<InvalidOperationException>(() => target.Show());
+                Assert.Equal("Cannot re-show a closed window.", ex.Message);
+                Assert.False(openedRaised);
+            }
+        }
+
+        [Fact]
+        public async Task Calling_ShowDialog_On_Closed_Window_Should_Throw()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var parent = new Mock<IWindowImpl>();
+                var windowImpl = new Mock<IWindowImpl>();
+                windowImpl.SetupProperty(x => x.Closed);
+                windowImpl.Setup(x => x.Scaling).Returns(1);
+
+                var target = new Window(windowImpl.Object);
+                var task = target.ShowDialog<bool>(parent.Object);
+
+                windowImpl.Object.Closed();
+                await task;
+
+                var openedRaised = false;
+                target.Opened += (s, e) => openedRaised = true;
+
+                var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => target.ShowDialog<bool>(parent.Object));
+                Assert.Equal("Cannot re-show a closed window.", ex.Message);
+                Assert.False(openedRaised);
+            }
+        }
+
         [Fact]
         public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen()
         {

+ 1 - 0
tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs

@@ -11,6 +11,7 @@ using DynamicData;
 using Xunit;
 using Splat;
 using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
 
 namespace Avalonia 
 {

+ 1 - 0
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@@ -14,6 +14,7 @@ using Avalonia.Markup.Xaml;
 using System.ComponentModel;
 using System.Threading.Tasks;
 using System.Reactive;
+using Avalonia.ReactiveUI;
 
 namespace Avalonia
 {

+ 1 - 7
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@@ -334,11 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if AVALONIA_SKIA_SKIP_FAIL
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public async Task Path_With_PenLineCap()
         {
             Decorator target = new Decorator
@@ -351,10 +347,8 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
                     StrokeThickness = 10,
                     HorizontalAlignment = HorizontalAlignment.Center,
                     VerticalAlignment = VerticalAlignment.Center,
-                    StrokeDashCap = PenLineCap.Triangle,
                     StrokeDashArray = new AvaloniaList<double>(3, 1),
-                    StrokeStartLineCap = PenLineCap.Round,
-                    StrokeEndLineCap = PenLineCap.Square,
+                    StrokeLineCap = PenLineCap.Round,
                     Data = StreamGeometry.Parse("M 20,20 L 180,180"),
                 }
             };

+ 1 - 2
tests/Avalonia.RenderTests/Shapes/PolylineTests.cs

@@ -61,8 +61,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
                     Points = polylinePoints,
                     Stretch = Stretch.Uniform,
                     StrokeJoin = PenLineJoin.Round,
-                    StrokeStartLineCap = PenLineCap.Round,
-                    StrokeEndLineCap = PenLineCap.Round,
+                    StrokeLineCap = PenLineCap.Round,
                     StrokeThickness = 10
                 }
             };

+ 15 - 0
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -22,6 +22,21 @@ namespace Avalonia.UnitTests
             return Mock.Of<IFormattedTextImpl>();
         }
 
+        public IGeometryImpl CreateEllipseGeometry(Rect rect)
+        {
+            return Mock.Of<IGeometryImpl>();
+        }
+
+        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
+        {
+            return Mock.Of<IGeometryImpl>();
+        }
+
+        public IGeometryImpl CreateRectangleGeometry(Rect rect)
+        {
+            return Mock.Of<IGeometryImpl>();
+        }
+
         public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
         {
             return Mock.Of<IRenderTarget>();

+ 15 - 0
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -56,6 +56,21 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
             throw new NotImplementedException();
         }
 
+        public IGeometryImpl CreateEllipseGeometry(Rect rect)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl CreateRectangleGeometry(Rect rect)
+        {
+            throw new NotImplementedException();
+        }
+
         class MockStreamGeometry : IStreamGeometryImpl
         {
             private MockStreamGeometryContext _impl = new MockStreamGeometryContext();

BIN
tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png


BIN
tests/TestFiles/Skia/Shapes/Path/Path_With_PenLineCap.expected.png