Browse Source

Merge branch 'master' into properties-refactor

Steven Kirk 9 years ago
parent
commit
afe0f9012c
48 changed files with 648 additions and 71 deletions
  1. 9 6
      docs/build.md
  2. 5 4
      samples/ControlCatalog/Pages/CanvasPage.paml
  3. 38 4
      samples/TestApplicationShared/MainWindow.cs
  4. 2 0
      src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs
  5. 37 0
      src/Markup/Perspex.Markup.Xaml/Converters/PointsListTypeConverter.cs
  6. 1 0
      src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
  7. 2 0
      src/Perspex.Controls/Perspex.Controls.csproj
  8. 29 11
      src/Perspex.Controls/Shapes/Line.cs
  9. 23 0
      src/Perspex.Controls/Shapes/Polygon.cs
  10. 28 0
      src/Perspex.Controls/Shapes/Polyline.cs
  11. 3 1
      src/Perspex.Controls/Shapes/Shape.cs
  12. 32 4
      src/Perspex.SceneGraph/Media/LineGeometry.cs
  13. 13 1
      src/Perspex.SceneGraph/Media/PathMarkupParser.cs
  14. 79 0
      src/Perspex.SceneGraph/Media/PolylineGeometry.cs
  15. 2 1
      src/Perspex.SceneGraph/Perspex.SceneGraph.csproj
  16. 3 17
      src/Skia/Perspex.Skia/DrawingContextImpl.cs
  17. 32 3
      src/Skia/Perspex.Skia/MethodTable.cs
  18. 59 0
      src/Skia/Perspex.Skia/PerspexHandleHolder.cs
  19. 36 4
      src/Skia/Perspex.Skia/StreamGeometryImpl.cs
  20. 8 2
      src/Skia/getnatives.sh
  21. 2 0
      tests/Perspex.RenderTests/Perspex.Cairo.RenderTests.csproj
  22. 2 0
      tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj
  23. 2 0
      tests/Perspex.RenderTests/Perspex.Skia.RenderTests.csproj
  24. 42 1
      tests/Perspex.RenderTests/Shapes/LineTests.cs
  25. 0 12
      tests/Perspex.RenderTests/Shapes/PathTests.cs
  26. 76 0
      tests/Perspex.RenderTests/Shapes/PolygonTests.cs
  27. 83 0
      tests/Perspex.RenderTests/Shapes/PolylineTests.cs
  28. BIN
      tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke.expected.png
  29. BIN
      tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke_Reversed.expected.png
  30. BIN
      tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke_Vertical.expected.png
  31. BIN
      tests/TestFiles/Cairo/Shapes/Polygon/Polygon_1px_Stroke.expected.png
  32. BIN
      tests/TestFiles/Cairo/Shapes/Polygon/Polygon_NonUniformFill.expected.png
  33. BIN
      tests/TestFiles/Cairo/Shapes/Polyline/Polyline_10px_Stroke_PenLineJoin.expected.png
  34. BIN
      tests/TestFiles/Cairo/Shapes/Polyline/Polyline_1px_Stroke.expected.png
  35. BIN
      tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke.expected.png
  36. BIN
      tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke_Reversed.expected.png
  37. BIN
      tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke_Vertical.expected.png
  38. BIN
      tests/TestFiles/Direct2D1/Shapes/Polygon/Polygon_1px_Stroke.expected.png
  39. BIN
      tests/TestFiles/Direct2D1/Shapes/Polygon/Polygon_NonUniformFill.expected.png
  40. BIN
      tests/TestFiles/Direct2D1/Shapes/Polyline/Polyline_10px_Stroke_PenLineJoin.expected.png
  41. BIN
      tests/TestFiles/Direct2D1/Shapes/Polyline/Polyline_1px_Stroke.expected.png
  42. BIN
      tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke.expected.png
  43. BIN
      tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke_Reversed.expected.png
  44. BIN
      tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke_Vertical.expected.png
  45. BIN
      tests/TestFiles/Skia/Shapes/Polygon/Polygon_1px_Stroke.expected.png
  46. BIN
      tests/TestFiles/Skia/Shapes/Polygon/Polygon_NonUniformFill.expected.png
  47. BIN
      tests/TestFiles/Skia/Shapes/Polyline/Polyline_10px_Stroke_PenLineJoin.expected.png
  48. BIN
      tests/TestFiles/Skia/Shapes/Polyline/Polyline_1px_Stroke.expected.png

+ 9 - 6
docs/build.md

@@ -7,8 +7,11 @@ Perspex requires Visual Studio 2015 to build on Windows.
 ### Install GTK Sharp
 
 To compile the full project under windows, you must have [gtk-sharp](http://www.mono-project.com/download/#download-win) installed. However, if you're 
-not interested in building the cross-platform bits you can simply unload the Perspex.Cairo and 
-Perspex.Gtk project in Visual Studio.
+not interested in building the cross-platform bits you can simply unload these projects from Visual Studio:
+
+ - Perspex.Cairo
+ - Perspex.Cairo.RenderTests
+ - Perspex.Gtk
 
 ### Clone the Perspex repository
 
@@ -19,7 +22,7 @@ is linked as a submodule in the git repository, so run:
 
     git submodule update --init
     
-The next step is to download the Skia native libraries. Run ```getnatives.ps1``` PowerShell script which can be found under the folder ```Perspex\src\Skia\```.
+The next step is to download the Skia native libraries. Run ```getnatives.ps1``` PowerShell script which can be found under the folder ```src\Skia\```.
 
 ## Linux
 
@@ -39,14 +42,14 @@ Then install the needed packages:
 
 ### Clone the Perspex repository
 
-    git clone https://github.com/grokys/Perspex.git
+    git clone https://github.com/Perspex/Perspex.git
 
 We currently need to build our own private version of ReactiveUI as it doesn't work on mono. This
 is linked as a submodule in the git repository, so run:
 
     git submodule update --init
     
-The next step is to download the Skia native libraries. Run ```getnatives.sh``` script which can be found under the folder ```Perspex\src\Skia\```.
+The next step is to download the Skia native libraries. Run ```getnatives.sh``` script which can be found under the folder ```src\Skia\```.
    
 ### Load the Project in MonoDevelop
 
@@ -57,4 +60,4 @@ Set the TestApplication project as the startup project and click Run.
 
 There will be some compile errors in the tests, but ignore them for now. 
 
-You can track the Linux version's progress in the [Linux issue](https://github.com/grokys/Perspex/issues/78).
+You can track the Linux version's progress in the [Linux issue](https://github.com/Perspex/Perspex/issues/78).

+ 5 - 4
samples/ControlCatalog/Pages/CanvasPage.paml

@@ -2,12 +2,13 @@
   <StackPanel Orientation="Vertical" Gap="4">
     <TextBlock Classes="h1">Canvas</TextBlock>
     <TextBlock Classes="h2">A panel which lays out its children by explicit coordinates</TextBlock>
-
-    <Canvas Background="Yellow" Width="300" Height="200">
+    <Canvas Background="Yellow" Width="300" Height="400">
       <Rectangle Fill="Blue" Width="63" Height="41" Canvas.Left="40" Canvas.Top="31"/>
       <Ellipse Fill="Green" Width="58" Height="58" Canvas.Left="160" Canvas.Top="79"/>
-      <Path Fill="Red" Data="M50,0 L0,50 100,50 Z" Canvas.Left="50" Canvas.Top="140"/>
+      <Path Fill="Orange" Data="M 0,0 c 50,0 50,-50 c 50,0 50,50 h -50 v 50 l -50,-50 Z" Canvas.Left="30" Canvas.Top="250"/>
+      <Line StartPoint="120,185" EndPoint="30,115" Stroke="Red" StrokeThickness="2"/>
+      <Polygon Points="75,0 120,120 0,45 150,45 30,120" Stroke="DarkBlue" StrokeThickness="1" Fill="Violet" Canvas.Left="150" Canvas.Top="180"/>
+      <Polyline Points="0,0 65,0 78,-26 91,39 104,-39 117,13 130,0 195,0" Stroke="Brown" Canvas.Left="30" Canvas.Top="350"/>
     </Canvas>
-    
   </StackPanel>
 </UserControl>

+ 38 - 4
samples/TestApplicationShared/MainWindow.cs

@@ -543,6 +543,17 @@ namespace TestApplication
 
         private static TabItem LayoutTab()
         {
+            var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3),
+                new Point(9, 1), new Point(10, 0), new Point(15, 0) };
+            var polygonPoints = new Point[] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) };
+            for (int i = 0; i < polylinePoints.Length; i++)
+            {
+                polylinePoints[i] = polylinePoints[i] * 13;
+            }
+            for (int i = 0; i < polygonPoints.Length; i++)
+            {
+                polygonPoints[i] = polygonPoints[i] * 15;
+            }
             return new TabItem
             {
                 Header = "Layout",
@@ -691,13 +702,36 @@ namespace TestApplication
                                     },
                                     new Line
                                     {
-                                        Width = 90,
-                                        Height = 70,
                                         Stroke = Brushes.Red,
                                         StrokeThickness = 2,
+                                        StartPoint = new Point(120, 185),
+                                        EndPoint = new Point(30, 115)
+                                    },
+                                    new Perspex.Controls.Shapes.Path
+                                    {
+                                        Fill = Brushes.Orange,
+                                        Data = StreamGeometry.Parse("M 30,250 c 50,0 50,-50 c 50,0 50,50 h -50 v 50 l -50,-50 Z"),
+                                    },
+                                    new Polygon
+                                    {
+                                        Stroke = Brushes.DarkBlue,
+                                        Fill = Brushes.Violet,
+                                        Points = polygonPoints,
+                                        StrokeThickness = 1,
+                                        [Canvas.LeftProperty] = 150,
+                                        [Canvas.TopProperty] = 180,
+                                    },
+                                    new Polyline
+                                    {
+                                        Stroke = Brushes.Brown,
+                                        Points = polylinePoints,
+                                        StrokeThickness = 5,
+                                        StrokeJoin = PenLineJoin.Round,
+                                        StrokeStartLineCap = PenLineCap.Triangle,
+                                        StrokeEndLineCap = PenLineCap.Triangle,
                                         [Canvas.LeftProperty] = 30,
-                                        [Canvas.TopProperty] = 120
-                                    }
+                                        [Canvas.TopProperty] = 350,
+                                    },
                                 }
                             },
                         }

+ 2 - 0
src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs

@@ -21,6 +21,7 @@ using Perspex.Media.Imaging;
 using Perspex.Metadata;
 using Perspex.Platform;
 using Perspex.Styling;
+using System.Collections.Generic;
 
 namespace Perspex.Markup.Xaml.Context
 {
@@ -101,6 +102,7 @@ namespace Perspex.Markup.Xaml.Context
                 new TypeConverterRegistration(typeof(PerspexList<double>), new PerspexListTypeConverter<double>()),
                 new TypeConverterRegistration(typeof(IMemberSelector), new MemberSelectorTypeConverter()),
                 new TypeConverterRegistration(typeof(Point), new PointTypeConverter()),
+                new TypeConverterRegistration(typeof(IList<Point>), new PointsListTypeConverter()),
                 new TypeConverterRegistration(typeof(PerspexProperty), new PerspexPropertyTypeConverter()),
                 new TypeConverterRegistration(typeof(RelativePoint), new RelativePointTypeConverter()),
                 new TypeConverterRegistration(typeof(RelativeRect), new RelativeRectTypeConverter()),

+ 37 - 0
src/Markup/Perspex.Markup.Xaml/Converters/PointsListTypeConverter.cs

@@ -0,0 +1,37 @@
+using OmniXaml.TypeConversion;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Perspex.Markup.Xaml.Converters
+{
+    public class PointsListTypeConverter : ITypeConverter
+    {
+        public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType)
+        {
+            return sourceType == typeof(string);
+        }
+
+        public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType)
+        {
+            return false;
+        }
+
+        public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value)
+        {
+            string strValue = (string)value;
+            string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+            var result = new List<Point>(pointStrs.Length);
+            foreach (var pointStr in pointStrs)
+            {
+                result.Add(Point.Parse(pointStr, culture));
+            }
+            return result;
+        }
+
+        public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 1 - 0
src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj

@@ -41,6 +41,7 @@
     <Compile Include="Context\NameScopeWrapper.cs" />
     <Compile Include="Converters\CursorTypeConverter.cs" />
     <Compile Include="Converters\PerspexListTypeConverter.cs" />
+    <Compile Include="Converters\PointsListTypeConverter.cs" />
     <Compile Include="Converters\RelativeRectTypeConverter.cs" />
     <Compile Include="Data\MultiBinding.cs" />
     <Compile Include="Data\RelativeSource.cs" />

+ 2 - 0
src/Perspex.Controls/Perspex.Controls.csproj

@@ -68,6 +68,8 @@
     <Compile Include="SelectionMode.cs" />
     <Compile Include="Separator.cs" />
     <Compile Include="Shapes\Line.cs" />
+    <Compile Include="Shapes\Polygon.cs" />
+    <Compile Include="Shapes\Polyline.cs" />
     <Compile Include="SystemDialog.cs" />
     <Compile Include="Generators\ITreeItemContainerGenerator.cs" />
     <Compile Include="Generators\ItemContainerEventArgs.cs" />

+ 29 - 11
src/Perspex.Controls/Shapes/Line.cs

@@ -9,28 +9,46 @@ namespace Perspex.Controls.Shapes
 {
     public class Line : Shape
     {
-        private Geometry _geometry;
+        public static readonly PerspexProperty<Point> StartPointProperty =
+            PerspexProperty.Register<Line, Point>("StartPoint");
 
-        private Size _geometrySize;
+        public static readonly PerspexProperty<Point> EndPointProperty =
+            PerspexProperty.Register<Line, Point>("EndPoint");
+
+        private LineGeometry _geometry;
+        private Point _startPoint;
+        private Point _endPoint;
+
+        static Line()
+        {
+            StrokeThicknessProperty.OverrideDefaultValue<Line>(1);
+        }
+
+        public Point StartPoint
+        {
+            get { return GetValue(StartPointProperty); }
+            set { SetValue(StartPointProperty, value); }
+        }
+
+        public Point EndPoint
+        {
+            get { return GetValue(EndPointProperty); }
+            set { SetValue(EndPointProperty, value); }
+        }
 
         public override Geometry DefiningGeometry
         {
             get
             {
-                if (_geometry == null || _geometrySize != Bounds.Size)
+                if (_geometry == null || StartPoint != _startPoint || EndPoint != _endPoint)
                 {
-                    var rect = new Rect(Bounds.Size).Deflate(StrokeThickness);
-                    _geometry = new LineGeometry(rect.TopLeft, rect.BottomRight);
-                    _geometrySize = Bounds.Size;
+                    _startPoint = StartPoint;
+                    _endPoint = EndPoint;
+                    _geometry = new LineGeometry(_startPoint, _endPoint);
                 }
 
                 return _geometry;
             }
         }
-
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            return new Size(StrokeThickness, StrokeThickness);
-        }
     }
 }

+ 23 - 0
src/Perspex.Controls/Shapes/Polygon.cs

@@ -0,0 +1,23 @@
+using Perspex.Media;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Perspex.Controls.Shapes
+{
+    public class Polygon : Shape
+    {
+        public static readonly PerspexProperty<IList<Point>> PointsProperty =
+            PerspexProperty.Register<Polygon, IList<Point>>("Points");
+
+        public IList<Point> Points
+        {
+            get { return GetValue(PointsProperty); }
+            set { SetValue(PointsProperty, value); }
+        }
+
+        public override Geometry DefiningGeometry => new PolylineGeometry(Points, true);
+    }
+}

+ 28 - 0
src/Perspex.Controls/Shapes/Polyline.cs

@@ -0,0 +1,28 @@
+using Perspex.Media;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Perspex.Controls.Shapes
+{
+    public class Polyline: Shape
+    {
+        public static readonly PerspexProperty<IList<Point>> PointsProperty =
+            PerspexProperty.Register<Polyline, IList<Point>>("Points");
+
+        static Polyline()
+        {
+            StrokeThicknessProperty.OverrideDefaultValue<Polyline>(1);
+        }
+
+        public IList<Point> Points
+        {
+            get { return GetValue(PointsProperty); }
+            set { SetValue(PointsProperty, value); }
+        }
+
+        public override Geometry DefiningGeometry => new PolylineGeometry(Points, false);
+    }
+}

+ 3 - 1
src/Perspex.Controls/Shapes/Shape.cs

@@ -96,6 +96,8 @@ namespace Perspex.Controls.Shapes
 
         public PenLineCap StrokeEndLineCap { get; set; } = PenLineCap.Flat;
 
+        public PenLineJoin StrokeJoin { get; set; } = PenLineJoin.Miter;
+
         public override void Render(DrawingContext context)
         {
             var geometry = RenderedGeometry;
@@ -103,7 +105,7 @@ namespace Perspex.Controls.Shapes
             if (geometry != null)
             {
                 var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray), 
-                    StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap);
+                    StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin);
                 context.DrawGeometry(Fill, pen, geometry);
             }
         }

+ 32 - 4
src/Perspex.SceneGraph/Media/LineGeometry.cs

@@ -27,8 +27,8 @@ namespace Perspex.Media
 
             using (IStreamGeometryContextImpl context = impl.Open())
             {
-                context.BeginFigure(startPoint, false);
-                context.LineTo(endPoint);
+                context.BeginFigure(_startPoint, false);
+                context.LineTo(_endPoint);
                 context.EndFigure(false);
             }
 
@@ -36,12 +36,40 @@ namespace Perspex.Media
         }
 
         /// <inheritdoc/>
-        public override Rect Bounds => new Rect(_startPoint, _endPoint);
+        public override Rect Bounds
+        {
+            get
+            {
+                double xMin, yMin, xMax, yMax;
+                if (_startPoint.X <= _endPoint.X)
+                {
+                    xMin = _startPoint.X;
+                    xMax = _endPoint.X;
+                }
+                else
+                {
+                    xMin = _endPoint.X;
+                    xMax = _startPoint.X;
+                }
+                if (_startPoint.Y <= _endPoint.Y)
+                {
+                    yMin = _startPoint.Y;
+                    yMax = _endPoint.Y;
+                }
+                else
+                {
+                    yMin = _endPoint.Y;
+                    yMax = _startPoint.Y;
+                }
+
+                return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
+            }
+        }
 
         /// <inheritdoc/>
         public override Geometry Clone()
         {
-            return new LineGeometry(Bounds.TopLeft, Bounds.BottomRight);
+            return new LineGeometry(_startPoint, _endPoint);
         }
     }
 }

+ 13 - 1
src/Perspex.SceneGraph/Media/PathMarkupParser.cs

@@ -147,6 +147,16 @@ namespace Perspex.Media
                                 _context.CubicBezierTo(point1, point2, point);
                                 break;
                             }
+
+                        case Command.CubicBezierCurveRelative:
+                            {
+                                Point point1 = ReadRelativePoint(reader, point);
+                                Point point2 = ReadRelativePoint(reader, point);
+                                _context.CubicBezierTo(point, point1, point2);
+                                point = point2;
+                                break;
+                            }
+
                         case Command.Arc:
                             {
                                 //example: A10,10 0 0,0 10,20
@@ -216,8 +226,10 @@ namespace Perspex.Media
             }
         }
 
-        private static double ReadDouble(TextReader reader)
+        private static double ReadDouble(StringReader reader)
         {
+            ReadWhitespace(reader);
+
             // TODO: Handle Infinity, NaN and scientific notation.
             StringBuilder b = new StringBuilder();
             bool readSign = false;

+ 79 - 0
src/Perspex.SceneGraph/Media/PolylineGeometry.cs

@@ -0,0 +1,79 @@
+using Perspex.Platform;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Perspex.Media
+{
+    /// <summary>
+    /// Represents the geometry of an polyline or polygon.
+    /// </summary>
+    public class PolylineGeometry : Geometry
+    {
+        private IList<Point> _points;
+        private bool _isFilled;
+
+        public PolylineGeometry(IList<Point> points, bool isFilled)
+        {
+            _points = points;
+            _isFilled = isFilled;
+            IPlatformRenderInterface factory = PerspexLocator.Current.GetService<IPlatformRenderInterface>();
+            IStreamGeometryImpl impl = factory.CreateStreamGeometry();
+
+            using (IStreamGeometryContextImpl context = impl.Open())
+            {
+                if (points.Count > 0)
+                {
+                    context.BeginFigure(points[0], isFilled);
+                    for (int i = 1; i < points.Count; i++)
+                    {
+                        context.LineTo(points[i]);
+                    }
+                    context.EndFigure(isFilled);
+                }
+            }
+
+            PlatformImpl = impl;
+        }
+
+        /// <inheritdoc/>
+        public override Rect Bounds
+        {
+            get
+            {
+                double xMin = double.MaxValue, yMin = double.MaxValue;
+                double xMax = double.MinValue, yMax = double.MinValue;
+                foreach (var point in _points)
+                {
+                    if (point.X < xMin)
+                    {
+                        xMin = point.X;
+                    }
+                    else if (point.X > xMax)
+                    {
+                        xMax = point.X;
+                    }
+
+                    if (point.Y < yMin)
+                    {
+                        yMin = point.Y;
+                    }
+                    else if (point.Y > yMax)
+                    {
+                        yMax = point.Y;
+                    }
+                }
+
+                return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
+            }
+        }
+
+        /// <inheritdoc/>
+        public override Geometry Clone()
+        {
+            return new PolylineGeometry(new List<Point>(_points), _isFilled);
+        }
+    }
+}

+ 2 - 1
src/Perspex.SceneGraph/Perspex.SceneGraph.csproj

@@ -72,11 +72,12 @@
     <Compile Include="Media\GradientSpreadMethod.cs" />
     <Compile Include="Media\GradientStop.cs" />
     <Compile Include="Media\LineGeometry.cs" />
+    <Compile Include="Media\PenLineJoin.cs" />
+    <Compile Include="Media\PolylineGeometry.cs" />
     <Compile Include="Media\RadialGradientBrush.cs" />
     <Compile Include="Media\LinearGradientBrush.cs" />
     <Compile Include="Media\MediaExtensions.cs" />
     <Compile Include="Media\PenLineCap.cs" />
-    <Compile Include="Media\PenLineJoin.cs" />
     <Compile Include="Media\TextAlignment.cs" />
     <Compile Include="Media\FontWeight.cs" />
     <Compile Include="Media\FontStyle.cs" />

+ 3 - 17
src/Skia/Perspex.Skia/DrawingContextImpl.cs

@@ -37,18 +37,13 @@ namespace Perspex.Skia
         public void DrawGeometry(Brush brush, Pen pen, Geometry geometry)
         {
             var impl = ((StreamGeometryImpl) geometry.PlatformImpl);
-            var oldTransform = Transform;
-            if (!impl.Transform.IsIdentity)
-                Transform = impl.Transform*Transform;
-            
             var size = geometry.Bounds.Size;
             using(var fill = brush!=null?CreateBrush(brush, size):null)
             using (var stroke = pen?.Brush != null ? CreateBrush(pen, size) : null)
             {
-                MethodTable.Instance.DrawGeometry(Handle, impl.Path.Handle, fill != null ? fill.Brush : null,
+                MethodTable.Instance.DrawGeometry(Handle, impl.EffectivePath, fill != null ? fill.Brush : null,
                     stroke != null ? stroke.Brush : null, impl.FillRule == FillRule.EvenOdd);
             }
-            Transform = oldTransform;
         }
 
         unsafe NativeBrushContainer CreateBrush(Brush brush, Size targetSize)
@@ -56,7 +51,6 @@ namespace Perspex.Skia
             var rv = NativeBrushPool.Instance.Get();
             rv.Brush->Opacity = brush.Opacity;
 
-            
             var solid = brush as SolidColorBrush;
             if (solid != null)
             {
@@ -108,8 +102,6 @@ namespace Perspex.Skia
                 rv.Brush->Bitmap = bitmap.Handle;
                 rv.Brush->BitmapTileMode = tileBrush.TileMode;
                 rv.Brush->BitmapTranslation = new SkiaPoint(-helper.DestinationRect.X, -helper.DestinationRect.Y);
-
-
             }
 
             return rv;
@@ -121,6 +113,7 @@ namespace Perspex.Skia
             brush.Brush->Stroke = true;
             brush.Brush->StrokeThickness = (float)pen.Thickness;
             brush.Brush->StrokeLineCap = pen.StartLineCap;
+            brush.Brush->StrokeLineJoin = pen.LineJoin;
             brush.Brush->StrokeMiterLimit = (float)pen.MiterLimit;
 
             if (pen.DashStyle?.Dashes != null)
@@ -196,14 +189,7 @@ namespace Perspex.Skia
                 if(_currentTransform == value)
                     return;
                 _currentTransform = value;
-                _fmatrix[0] = (float)value.M11;
-                _fmatrix[1] = (float)value.M21;
-                _fmatrix[2] = (float)value.M31;
-
-                _fmatrix[3] = (float)value.M12;
-                _fmatrix[4] = (float)value.M22;
-                _fmatrix[5] = (float)value.M32;
-                MethodTable.Instance.SetTransform(Handle, _fmatrix);
+                MethodTable.Instance.SetTransform(Handle, value);
             } 
         }
     }

+ 32 - 3
src/Skia/Perspex.Skia/MethodTable.cs

@@ -47,9 +47,9 @@ namespace Perspex.Skia
         public _PopClip PopClip;
 
         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
-        public delegate void _SetTransform(IntPtr ctx, float[] matrix6);
+        public delegate void _SetTransform(IntPtr ctx, void* matrix6);
 
-        public _SetTransform SetTransform;
+        public _SetTransform SetTransformNative;
 
         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
         public delegate void _DrawLine(IntPtr ctx, void* brush, float x1, float y1, float x2, float y2);
@@ -66,6 +66,11 @@ namespace Perspex.Skia
 
         public _DisposePath DisposePath;
 
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+        public delegate IntPtr _TransformPath(IntPtr path, void* matrix6);
+
+        public _TransformPath TransformPathNative;
+
         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
         public delegate void _DrawGeometry(IntPtr ctx, IntPtr path, void* fill, void* stroke, bool useEvenOdd);
 
@@ -185,10 +190,34 @@ namespace Perspex.Skia
             typeof (_RebuildFormattedText),
             typeof (_DestroyFormattedText),
             typeof (_DrawFormattedText),
-            typeof (_SetOption)
+            typeof (_SetOption),
+            typeof (_TransformPath)
         };
 
+        void ConvertMatrix(Matrix value, float* target)
+        {
+            target[0] = (float)value.M11;
+            target[1] = (float)value.M21;
+            target[2] = (float)value.M31;
+
+            target[3] = (float)value.M12;
+            target[4] = (float)value.M22;
+            target[5] = (float)value.M32;
+        }
 
+        public unsafe IntPtr TransformPath(IntPtr path, Matrix matrix)
+        {
+            var tmp = stackalloc float[6];
+            ConvertMatrix(matrix, tmp);
+            return TransformPathNative(path, tmp);
+        }
+
+        public unsafe void SetTransform(IntPtr ctx, Matrix matrix)
+        {
+            var tmp = stackalloc float[6];
+            ConvertMatrix(matrix, tmp);
+            SetTransformNative(ctx, tmp);
+        }
 
         protected MethodTable(IntPtr methodTable)
         {

+ 59 - 0
src/Skia/Perspex.Skia/PerspexHandleHolder.cs

@@ -44,4 +44,63 @@ namespace Perspex.Skia
             Dispose();
         }
     }
+
+    class RefCountable<T> : IDisposable where T : PerspexHandleHolder
+    {
+        class Shared
+        {
+            public readonly T Target;
+            private int _refCount = 1;
+
+            public Shared(T target)
+            {
+                Target = target;
+            }
+
+            public void AddRef() => _refCount++;
+            public void Release()
+            {
+                _refCount--;
+                if (_refCount <= 0)
+                    Target.Dispose();
+            }
+        }
+
+        public bool IsDisposed => _shared == null;
+        private Shared _shared;
+        public void CheckDisposed()
+        {
+            if (IsDisposed)
+                throw new ObjectDisposedException(GetType().FullName);
+        }
+
+        public IntPtr Handle
+        {
+            get
+            {
+                CheckDisposed();
+                return _shared.Target.Handle;
+            }
+        }
+
+        public RefCountable(T handle)
+        {
+            _shared = new Shared(handle);
+        }
+
+        public RefCountable(RefCountable<T> other)
+        {
+            other._shared.Target.CheckDisposed();
+            other._shared.AddRef();
+            _shared = other._shared;
+        }
+
+        public RefCountable<T> Clone() => new RefCountable<T>(this);
+
+        public void Dispose()
+        {
+            _shared?.Release();
+            _shared = null;
+        }
+    }
 }

+ 36 - 4
src/Skia/Perspex.Skia/StreamGeometryImpl.cs

@@ -41,7 +41,11 @@ namespace Perspex.Skia
 
     class StreamGeometryImpl : IStreamGeometryImpl
     {
-        public SkPath Path;
+        RefCountable<SkPath> _path;
+        RefCountable<SkPath> _transformedPath;
+        private Matrix _transform = Matrix.Identity;
+        
+        public IntPtr EffectivePath => (_transformedPath ?? _path).Handle;
 
         public Rect GetRenderBounds(double strokeThickness)
         {
@@ -51,11 +55,35 @@ namespace Perspex.Skia
 
         public Rect Bounds { get; private set; }
 
-        public Matrix Transform { get; set; } = Matrix.Identity;
+        public Matrix Transform
+        {
+            get { return _transform; }
+            set
+            {
+                if(_transform == value)
+                    return;
+                _transform = value;
+                ApplyTransform();
+            }
+        }
+
+        void ApplyTransform()
+        {
+            if(_path == null)
+                return;
+            if (_transformedPath != null)
+            {
+                _transformedPath.Dispose();
+                _transformedPath = null;
+            }
+            if (!_transform.IsIdentity)
+                _transformedPath =
+                    new RefCountable<SkPath>(new SkPath(MethodTable.Instance.TransformPath(_path.Handle, Transform)));
+        }
 
         public IStreamGeometryImpl Clone()
         {
-            return new StreamGeometryImpl() {Path = Path, Transform = Transform, Bounds = Bounds};
+            return new StreamGeometryImpl {_path = _path?.Clone(), _transformedPath = _transformedPath?.Clone(), _transform = Transform, Bounds = Bounds};
         }
 
         public IStreamGeometryContextImpl Open()
@@ -77,7 +105,11 @@ namespace Perspex.Skia
             {
                 var arr = _elements.ToArray();
                 SkRect rc;
-                _geometryImpl.Path = new SkPath(MethodTable.Instance.CreatePath(arr, arr.Length, out rc));
+                _geometryImpl._path?.Dispose();
+                _geometryImpl._path =
+                    new RefCountable<SkPath>(new SkPath(MethodTable.Instance.CreatePath(arr, arr.Length, out rc)));
+                _geometryImpl.ApplyTransform();
+
                 _geometryImpl.Bounds = rc.ToRect();
 
             }

+ 8 - 2
src/Skia/getnatives.sh

@@ -1,7 +1,13 @@
 #!/bin/sh
-rm -rf native
+rm -rf native native.zip
 mkdir -p native
 cd native
+if which curl
+then
+curl `cat ../native.url` -o native.zip
+else
 wget `cat ../native.url` -O native.zip
-unzip native.zip
+fi
 
+unzip native.zip
+chmod -R +x .

+ 2 - 0
tests/Perspex.RenderTests/Perspex.Cairo.RenderTests.csproj

@@ -74,6 +74,8 @@
     <Compile Include="Shapes\LineTests.cs" />
     <Compile Include="Shapes\PathTests.cs" />
     <Compile Include="Shapes\EllipseTests.cs" />
+    <Compile Include="Shapes\PolygonTests.cs" />
+    <Compile Include="Shapes\PolylineTests.cs" />
     <Compile Include="Shapes\RectangleTests.cs" />
     <Compile Include="TestBase.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />

+ 2 - 0
tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj

@@ -79,6 +79,8 @@
     <Compile Include="Shapes\LineTests.cs" />
     <Compile Include="Shapes\PathTests.cs" />
     <Compile Include="Shapes\EllipseTests.cs" />
+    <Compile Include="Shapes\PolygonTests.cs" />
+    <Compile Include="Shapes\PolylineTests.cs" />
     <Compile Include="Shapes\RectangleTests.cs" />
     <Compile Include="TestBase.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />

+ 2 - 0
tests/Perspex.RenderTests/Perspex.Skia.RenderTests.csproj

@@ -70,6 +70,8 @@
     <Compile Include="Shapes\EllipseTests.cs" />
     <Compile Include="Shapes\LineTests.cs" />
     <Compile Include="Shapes\PathTests.cs" />
+    <Compile Include="Shapes\PolygonTests.cs" />
+    <Compile Include="Shapes\PolylineTests.cs" />
     <Compile Include="Shapes\RectangleTests.cs" />
     <Compile Include="TestBase.cs" />
   </ItemGroup>

+ 42 - 1
tests/Perspex.RenderTests/Shapes/LineTests.cs

@@ -26,13 +26,54 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
         {
             Decorator target = new Decorator
             {
-                Padding = new Thickness(8),
                 Width = 200,
                 Height = 200,
                 Child = new Line
                 {
                     Stroke = Brushes.Black,
                     StrokeThickness = 1,
+                    StartPoint = new Point(0, 0),
+                    EndPoint = new Point(200, 200)
+                }
+            };
+
+            RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public void Line_1px_Stroke_Reversed()
+        {
+            Decorator target = new Decorator
+            {
+                Width = 200,
+                Height = 200,
+                Child = new Line
+                {
+                    Stroke = Brushes.Black,
+                    StrokeThickness = 1,
+                    StartPoint = new Point(200, 0),
+                    EndPoint = new Point(0, 200)
+                }
+            };
+
+            RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public void Line_1px_Stroke_Vertical()
+        {
+            Decorator target = new Decorator
+            {
+                Width = 200,
+                Height = 200,
+                Child = new Line
+                {
+                    Stroke = Brushes.Black,
+                    StrokeThickness = 1,
+                    StartPoint = new Point(100, 200),
+                    EndPoint = new Point(100, 0)
                 }
             };
 

+ 0 - 12
tests/Perspex.RenderTests/Shapes/PathTests.cs

@@ -46,11 +46,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if PERSPEX_SKIA
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public void Path_Tick_Scaled()
         {
             Decorator target = new Decorator
@@ -73,11 +69,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if PERSPEX_SKIA
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public void Path_Tick_Scaled_Stroke_8px()
         {
             Decorator target = new Decorator
@@ -100,11 +92,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if PERSPEX_SKIA
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public void Path_Expander_With_Border()
         {
             Decorator target = new Decorator

+ 76 - 0
tests/Perspex.RenderTests/Shapes/PolygonTests.cs

@@ -0,0 +1,76 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Perspex.Controls;
+using Perspex.Controls.Shapes;
+using Perspex.Media;
+using Xunit;
+
+#if PERSPEX_CAIRO
+namespace Perspex.Cairo.RenderTests.Shapes
+#elif PERSPEX_SKIA
+namespace Perspex.Skia.RenderTests
+#else
+namespace Perspex.Direct2D1.RenderTests.Shapes
+#endif
+{
+    public class PolygonTests : TestBase
+    {
+        public PolygonTests()
+            : base(@"Shapes\Polygon")
+        {
+        }
+
+#if PERSPEX_CAIRO
+        [Fact(Skip = "Caused by cairo bug")]
+#else
+        [Fact]
+#endif
+        public void Polygon_1px_Stroke()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Polygon
+                {
+                    Stroke = Brushes.DarkBlue,
+                    Stretch = Stretch.Uniform,
+                    Fill = Brushes.Violet,
+                    Points = new [] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) },
+                    StrokeThickness = 1
+                }
+            };
+
+            RenderToFile(target);
+            CompareImages();
+        }
+
+#if PERSPEX_CAIRO
+        [Fact(Skip = "Caused by cairo bug")]
+#else
+        [Fact]
+#endif
+        public void Polygon_NonUniformFill()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 400,
+                Height = 200,
+                Child = new Polygon
+                {
+                    Stroke = Brushes.DarkBlue,
+                    Stretch = Stretch.Fill,
+                    Fill = Brushes.Violet,
+                    Points = new[] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) },
+                    StrokeThickness = 5,
+                }
+            };
+
+            RenderToFile(target);
+            CompareImages();
+        }
+    }
+}

+ 83 - 0
tests/Perspex.RenderTests/Shapes/PolylineTests.cs

@@ -0,0 +1,83 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Perspex.Controls;
+using Perspex.Controls.Shapes;
+using Perspex.Media;
+using Xunit;
+
+#if PERSPEX_CAIRO
+namespace Perspex.Cairo.RenderTests.Shapes
+#elif PERSPEX_SKIA
+namespace Perspex.Skia.RenderTests
+#else
+namespace Perspex.Direct2D1.RenderTests.Shapes
+#endif
+{
+    public class PolylineTests : TestBase
+    {
+        public PolylineTests()
+            : base(@"Shapes\Polyline")
+        {
+        }
+
+#if PERSPEX_CAIRO
+        [Fact(Skip = "Caused by cairo bug")]
+#else
+        [Fact]
+#endif
+        public void Polyline_1px_Stroke()
+        {
+            var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3),
+                new Point(9, 1), new Point(10, 0), new Point(15, 0) };
+
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 400,
+                Height = 200,
+                Child = new Polyline
+                {
+                    Stroke = Brushes.Brown,
+                    Points = polylinePoints,
+                    Stretch = Stretch.Uniform,
+                    StrokeThickness = 1
+                }
+            };
+
+            RenderToFile(target);
+            CompareImages();
+        }
+
+#if PERSPEX_CAIRO
+        [Fact(Skip = "Caused by cairo bug")]
+#else
+        [Fact]
+#endif
+        public void Polyline_10px_Stroke_PenLineJoin()
+        {
+            var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3),
+                new Point(9, 1), new Point(10, 0), new Point(15, 0) };
+
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 400,
+                Height = 200,
+                Child = new Polyline
+                {
+                    Stroke = Brushes.Brown,
+                    Points = polylinePoints,
+                    Stretch = Stretch.Uniform,
+                    StrokeJoin = PenLineJoin.Round,
+                    StrokeStartLineCap = PenLineCap.Round,
+                    StrokeEndLineCap = PenLineCap.Round,
+                    StrokeThickness = 10
+                }
+            };
+
+            RenderToFile(target);
+            CompareImages();
+        }
+    }
+}

BIN
tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke.expected.png


BIN
tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke_Reversed.expected.png


BIN
tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke_Vertical.expected.png


BIN
tests/TestFiles/Cairo/Shapes/Polygon/Polygon_1px_Stroke.expected.png


BIN
tests/TestFiles/Cairo/Shapes/Polygon/Polygon_NonUniformFill.expected.png


BIN
tests/TestFiles/Cairo/Shapes/Polyline/Polyline_10px_Stroke_PenLineJoin.expected.png


BIN
tests/TestFiles/Cairo/Shapes/Polyline/Polyline_1px_Stroke.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke_Reversed.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke_Vertical.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Polygon/Polygon_1px_Stroke.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Polygon/Polygon_NonUniformFill.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Polyline/Polyline_10px_Stroke_PenLineJoin.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Polyline/Polyline_1px_Stroke.expected.png


BIN
tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke.expected.png


BIN
tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke_Reversed.expected.png


BIN
tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke_Vertical.expected.png


BIN
tests/TestFiles/Skia/Shapes/Polygon/Polygon_1px_Stroke.expected.png


BIN
tests/TestFiles/Skia/Shapes/Polygon/Polygon_NonUniformFill.expected.png


BIN
tests/TestFiles/Skia/Shapes/Polyline/Polyline_10px_Stroke_PenLineJoin.expected.png


BIN
tests/TestFiles/Skia/Shapes/Polyline/Polyline_1px_Stroke.expected.png