Browse Source

Merge pull request #2 from AvaloniaUI/master

Update fork
Matthijs ter Woord 8 years ago
parent
commit
90e6bf1b4d
30 changed files with 771 additions and 114 deletions
  1. 1 0
      samples/RenderTest/MainWindow.xaml
  2. 132 0
      samples/RenderTest/Pages/DrawingPage.xaml
  3. 18 0
      samples/RenderTest/Pages/DrawingPage.xaml.cs
  4. 8 0
      samples/RenderTest/RenderTest.csproj
  5. 59 0
      src/Avalonia.Controls/DrawingPresenter.cs
  6. 16 12
      src/Avalonia.Controls/Shapes/Shape.cs
  7. 29 0
      src/Avalonia.Visuals/Matrix.cs
  8. 9 0
      src/Avalonia.Visuals/Media/Drawing.cs
  9. 58 0
      src/Avalonia.Visuals/Media/DrawingGroup.cs
  10. 39 12
      src/Avalonia.Visuals/Media/EllipseGeometry.cs
  11. 43 0
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  12. 67 13
      src/Avalonia.Visuals/Media/LineGeometry.cs
  13. 1 0
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  14. 98 17
      src/Avalonia.Visuals/Media/PolylineGeometry.cs
  15. 39 12
      src/Avalonia.Visuals/Media/RectangleGeometry.cs
  16. 1 1
      src/Avalonia.Visuals/Point.cs
  17. 9 0
      src/Avalonia.Visuals/Points.cs
  18. 26 0
      src/Avalonia.Visuals/Rect.cs
  19. 1 1
      src/Avalonia.Visuals/RelativeRect.cs
  20. 24 18
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  21. 6 0
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  22. 2 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  23. 23 0
      src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs
  24. 23 0
      src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs
  25. 2 0
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
  26. 4 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  27. 16 0
      tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs
  28. 1 0
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs
  29. 16 0
      tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs
  30. 0 27
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs

+ 1 - 0
samples/RenderTest/MainWindow.xaml

@@ -27,6 +27,7 @@
       </TabControl.Transition>
       <TabItem Header="Animations"><pages:AnimationsPage/></TabItem>
       <TabItem Header="Clipping"><pages:ClippingPage/></TabItem>
+      <TabItem Header="Drawing"><pages:DrawingPage/></TabItem>
     </TabControl>
   </DockPanel>
 </Window>

+ 132 - 0
samples/RenderTest/Pages/DrawingPage.xaml

@@ -0,0 +1,132 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <UserControl.Styles>
+        <Style>
+            <Style.Resources>
+                <DrawingGroup x:Key="Bulb">
+                    <DrawingGroup.Transform>
+                        <MatrixTransform Matrix="1,0,0,1,0,-1028.4" />
+                    </DrawingGroup.Transform>
+                    <DrawingGroup>
+                        <DrawingGroup.Transform>
+                            <MatrixTransform Matrix="1,0,0,1.25,-10,1031.4" />
+                        </DrawingGroup.Transform>
+                        <GeometryDrawing Brush="#FF7F8C8D"
+                                         Geometry="F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z" />
+                    </DrawingGroup>
+                    <GeometryDrawing Brush="#FFF39C12"
+                                     Geometry="F1 M12,1030.4 C8.134,1030.4 5,1033.6 5,1037.6 5,1040.7 8.125,1043.5 9,1045.4 9.875,1047.2 9,1050.4 9,1050.4 L12,1049.9 15,1050.4 C15,1050.4 14.125,1047.2 15,1045.4 15.875,1043.5 19,1040.7 19,1037.6 19,1033.6 15.866,1030.4 12,1030.4 z" />
+                    <GeometryDrawing Brush="#FFF1C40F"
+                                     Geometry="F1 M12,1030.4 C15.866,1030.4 19,1033.6 19,1037.6 19,1040.7 15.875,1043.5 15,1045.4 14.125,1047.2 15,1050.4 15,1050.4 L12,1049.9 12,1030.4 z" />
+                    <GeometryDrawing Brush="#FFE67E22"
+                                     Geometry="F1 M9,1036.4 L8,1037.4 12,1049.4 16,1037.4 15,1036.4 14,1037.4 13,1036.4 12,1037.4 11,1036.4 10,1037.4 9,1036.4 z M9,1037.4 L10,1038.4 10.5,1037.9 11,1037.4 11.5,1037.9 12,1038.4 12.5,1037.9 13,1037.4 13.5,1037.9 14,1038.4 15,1037.4 15.438,1037.8 12,1048.1 8.5625,1037.8 9,1037.4 z" />
+                    <DrawingGroup>
+                        <DrawingGroup.Transform>
+                            <MatrixTransform Matrix="1,0,0,1,9,1045.4" />
+                        </DrawingGroup.Transform>
+                        <GeometryDrawing Brush="#FFBDC3C7">
+                            <GeometryDrawing.Geometry>
+                                <RectangleGeometry Rect="0,0,6,5" />
+                            </GeometryDrawing.Geometry>
+                        </GeometryDrawing>
+                    </DrawingGroup>
+                    <GeometryDrawing Brush="#FF95A5A6"
+                                     Geometry="F1 M9,1045.4 L9,1050.4 12,1050.4 12,1049.4 15,1049.4 15,1048.4 12,1048.4 12,1047.4 15,1047.4 15,1046.4 12,1046.4 12,1045.4 9,1045.4 z" />
+                    <GeometryDrawing Brush="#FF7F8C8D"
+                                     Geometry="F1 M9,1046.4 L9,1047.4 12,1047.4 12,1046.4 9,1046.4 z M9,1048.4 L9,1049.4 12,1049.4 12,1048.4 9,1048.4 z" />
+                </DrawingGroup>
+            </Style.Resources>
+        </Style>
+    </UserControl.Styles>
+    <Grid RowDefinitions="Auto,Auto,Auto"
+          ColumnDefinitions="Auto,Auto,Auto,Auto">
+        <TextBlock Text="None"
+                   Margin="3" />
+        <Border Grid.Column="0"
+                Grid.Row="1"
+                VerticalAlignment="Top"
+                HorizontalAlignment="Left"
+                BorderThickness="1"
+                BorderBrush="Gray"
+                Margin="5">
+            <DrawingPresenter Drawing="{StyleResource Bulb}" />
+        </Border>
+        <TextBlock Text="Fill"
+                   Margin="3"
+                   Grid.Column="1" />
+        <Border Grid.Column="1"
+                Grid.Row="1"
+                VerticalAlignment="Top"
+                HorizontalAlignment="Left"
+                BorderThickness="1"
+                BorderBrush="Gray"
+                Margin="5">
+            <DrawingPresenter Drawing="{StyleResource Bulb}"
+                              Width="100"
+                              Height="50"
+                              Stretch="Fill" />
+        </Border>
+        <TextBlock Text="Uniform"
+                   Margin="3"
+                   Grid.Column="2" />
+        <Border Grid.Column="2"
+                Grid.Row="1"
+                VerticalAlignment="Top"
+                HorizontalAlignment="Left"
+                BorderThickness="1"
+                BorderBrush="Gray"
+                Margin="5">
+            <DrawingPresenter Drawing="{StyleResource Bulb}"
+                              Width="100"
+                              Height="50"
+                              Stretch="Uniform" />
+        </Border>
+        <TextBlock Text="UniformToFill"
+                   Margin="3"
+                   Grid.Column="3" />
+        <Border Grid.Column="3"
+                Grid.Row="1"
+                VerticalAlignment="Top"
+                HorizontalAlignment="Left"
+                BorderThickness="1"
+                BorderBrush="Gray"
+                Margin="5">
+            <DrawingPresenter Drawing="{StyleResource Bulb}"
+                              Width="100"
+                              Height="50"
+                              Stretch="UniformToFill" />
+        </Border>
+
+        <!-- For comparison -->
+        
+        <Ellipse Grid.Row="2"
+                 Grid.Column="0"
+                 Width="100"
+                 Height="50"
+                 Stretch="None"
+                 Fill="Blue"
+                 Margin="5"/>
+        <Ellipse Grid.Row="2"
+                 Grid.Column="1"
+                 Width="100"
+                 Height="50"
+                 Stretch="Fill"
+                 Fill="Blue"
+                 Margin="5" />
+        <Ellipse Grid.Row="2"
+                 Grid.Column="2"
+                 Width="100"
+                 Height="50"
+                 Stretch="Uniform"
+                 Fill="Blue"
+                 Margin="5" />
+        <Ellipse Grid.Row="2"
+                 Grid.Column="3"
+                 Width="100"
+                 Height="50"
+                 Stretch="UniformToFill"
+                 Fill="Blue"
+                 Margin="5" />
+        
+    </Grid>
+</UserControl>

+ 18 - 0
samples/RenderTest/Pages/DrawingPage.xaml.cs

@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace RenderTest.Pages
+{
+    public class DrawingPage : UserControl
+    {
+        public DrawingPage()
+        {
+            InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 8 - 0
samples/RenderTest/RenderTest.csproj

@@ -51,6 +51,9 @@
     <Compile Include="App.xaml.cs">
       <DependentUpon>App.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\DrawingPage.xaml.cs">
+      <DependentUpon>DrawingPage.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\ClippingPage.xaml.cs">
       <DependentUpon>ClippingPage.xaml</DependentUpon>
     </Compile>
@@ -178,6 +181,11 @@
       <SubType>Designer</SubType>
     </EmbeddedResource>
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Pages\DrawingPage.xaml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="..\..\build\Serilog.props" />
   <Import Project="..\..\build\Serilog.Sinks.Trace.props" />

+ 59 - 0
src/Avalonia.Controls/DrawingPresenter.cs

@@ -0,0 +1,59 @@
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls
+{
+    public class DrawingPresenter : Control
+    {
+        static DrawingPresenter()
+        {
+            AffectsMeasure(DrawingProperty);
+            AffectsRender(DrawingProperty);
+        }
+
+        public static readonly StyledProperty<Drawing> DrawingProperty =
+            AvaloniaProperty.Register<DrawingPresenter, Drawing>(nameof(Drawing));
+
+        public static readonly StyledProperty<Stretch> StretchProperty =
+            AvaloniaProperty.Register<DrawingPresenter, Stretch>(nameof(Stretch), Stretch.Uniform);
+
+        [Content]
+        public Drawing Drawing
+        {
+            get => GetValue(DrawingProperty);
+            set => SetValue(DrawingProperty, value);
+        }
+
+        public Stretch Stretch
+        {
+            get => GetValue(StretchProperty);
+            set => SetValue(StretchProperty, value);
+        }
+
+        private Matrix _transform = Matrix.Identity;
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            if (Drawing == null) return new Size();
+
+            var (size, transform) = Shape.CalculateSizeAndTransform(availableSize, Drawing.GetBounds(), Stretch);
+
+            _transform = transform;
+
+            return size;
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            if (Drawing != null)
+            {
+                using (context.PushPreTransform(_transform))
+                using (context.PushClip(Bounds))
+                {
+                    Drawing.Draw(context);
+                }
+            }
+        }
+    }
+}

+ 16 - 12
src/Avalonia.Controls/Shapes/Shape.cs

@@ -155,11 +155,21 @@ namespace Avalonia.Controls.Shapes
         {
             // This should probably use GetRenderBounds(strokeThickness) but then the calculations
             // will multiply the stroke thickness as well, which isn't correct.
-            Rect shapeBounds = DefiningGeometry.Bounds;
+            var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch);
+            
+            if (_transform != transform)
+            {
+                _transform = transform;
+                _renderedGeometry = null;
+            }
+
+            return size;
+        }
+
+        internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch)
+        {
             Size shapeSize = new Size(shapeBounds.Right, shapeBounds.Bottom);
             Matrix translate = Matrix.Identity;
-            double width = Width;
-            double height = Height;
             double desiredX = availableSize.Width;
             double desiredY = availableSize.Height;
             double sx = 0.0;
@@ -226,15 +236,9 @@ namespace Avalonia.Controls.Shapes
                     break;
             }
 
-            var t = translate * Matrix.CreateScale(sx, sy);
-
-            if (_transform != t)
-            {
-                _transform = t;
-                _renderedGeometry = null;
-            }
-
-            return new Size(shapeSize.Width * sx, shapeSize.Height * sy);
+            var transform = translate * Matrix.CreateScale(sx, sy);
+            var size = new Size(shapeSize.Width * sx, shapeSize.Height * sy);
+            return (size, transform);
         }
 
         private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)

+ 29 - 0
src/Avalonia.Visuals/Matrix.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Globalization;
+using System.Linq;
 
 namespace Avalonia
 {
@@ -295,5 +296,33 @@ namespace Avalonia
                 ((_m21 * _m32) - (_m22 * _m31)) / d,
                 ((_m12 * _m31) - (_m11 * _m32)) / d);
         }
+
+        /// <summary>
+        /// Parses a <see cref="Matrix"/> string.
+        /// </summary>
+        /// <param name="s">The string.</param>
+        /// <param name="culture">The current culture.</param>
+        /// <returns>The <see cref="Matrix"/>.</returns>
+        public static Matrix Parse(string s, CultureInfo culture)
+        {
+            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(x => x.Trim())
+                .ToArray();
+
+            if (parts.Length == 6)
+            {
+                return new Matrix(
+                    double.Parse(parts[0], culture), 
+                    double.Parse(parts[1], culture), 
+                    double.Parse(parts[2], culture), 
+                    double.Parse(parts[3], culture), 
+                    double.Parse(parts[4], culture), 
+                    double.Parse(parts[5], culture));
+            }
+            else
+            {
+                throw new FormatException("Invalid Matrix.");
+            }
+        }
     }
 }

+ 9 - 0
src/Avalonia.Visuals/Media/Drawing.cs

@@ -0,0 +1,9 @@
+namespace Avalonia.Media
+{
+    public abstract class Drawing : AvaloniaObject
+    {
+        public abstract void Draw(DrawingContext context);
+
+        public abstract Rect GetBounds();
+    }
+}

+ 58 - 0
src/Avalonia.Visuals/Media/DrawingGroup.cs

@@ -0,0 +1,58 @@
+using Avalonia.Collections;
+using Avalonia.Metadata;
+
+namespace Avalonia.Media
+{
+    public class DrawingGroup : Drawing
+    {
+        public static readonly StyledProperty<double> OpacityProperty =
+            AvaloniaProperty.Register<DrawingGroup, double>(nameof(Opacity), 1);
+
+        public static readonly StyledProperty<Transform> TransformProperty =
+            AvaloniaProperty.Register<DrawingGroup, Transform>(nameof(Transform));
+
+        public double Opacity
+        {
+            get => GetValue(OpacityProperty);
+            set => SetValue(OpacityProperty, value);
+        }
+
+        public Transform Transform
+        {
+            get => GetValue(TransformProperty);
+            set => SetValue(TransformProperty, value);
+        }
+
+        [Content]
+        public AvaloniaList<Drawing> Children { get; } = new AvaloniaList<Drawing>();
+
+        public override void Draw(DrawingContext context)
+        {
+            using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
+            using (context.PushOpacity(Opacity))
+            {
+                foreach (var drawing in Children)
+                {
+                    drawing.Draw(context);
+                }
+            }
+        }
+
+        public override Rect GetBounds()
+        {
+            var rect = new Rect();
+
+            foreach (var drawing in Children)
+            {
+                rect = rect.Union(drawing.GetBounds());
+            }
+
+            if (Transform != null)
+            {
+                rect = rect.TransformToAABB(Transform.Value);
+            }
+
+            return rect;
+        }
+    }
+}

+ 39 - 12
src/Avalonia.Visuals/Media/EllipseGeometry.cs

@@ -11,16 +11,51 @@ namespace Avalonia.Media
     /// </summary>
     public class EllipseGeometry : Geometry
     {
+        /// <summary>
+        /// Defines the <see cref="Rect"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Rect> RectProperty =
+            AvaloniaProperty.Register<EllipseGeometry, Rect>(nameof(Rect));
+
+        public Rect Rect
+        {
+            get => GetValue(RectProperty);
+            set => SetValue(RectProperty, value);
+        }
+
+        static EllipseGeometry()
+        {
+            RectProperty.Changed.AddClassHandler<EllipseGeometry>(x => x.RectChanged);
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="EllipseGeometry"/> class.
         /// </summary>
-        /// <param name="rect">The rectangle that the ellipse should fill.</param>
-        public EllipseGeometry(Rect rect)
+        public EllipseGeometry()
         {
             IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            IStreamGeometryImpl impl = factory.CreateStreamGeometry();
+            PlatformImpl = factory.CreateStreamGeometry();
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="EllipseGeometry"/> class.
+        /// </summary>
+        /// <param name="rect">The rectangle that the ellipse should fill.</param>
+        public EllipseGeometry(Rect rect) : this()
+        {
+            Rect = rect;
+        }
+
+        /// <inheritdoc/>
+        public override Geometry Clone()
+        {
+            return new EllipseGeometry(Rect);
+        }
 
-            using (IStreamGeometryContextImpl ctx = impl.Open())
+        private void RectChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var rect = (Rect)e.NewValue;
+            using (var ctx = ((IStreamGeometryImpl)PlatformImpl).Open())
             {
                 double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3;
                 var center = rect.Center;
@@ -45,14 +80,6 @@ namespace Avalonia.Media
                 ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0));
                 ctx.EndFigure(true);
             }
-
-            PlatformImpl = impl;
-        }
-
-        /// <inheritdoc/>
-        public override Geometry Clone()
-        {
-            return new EllipseGeometry(Bounds);
         }
     }
 }

+ 43 - 0
src/Avalonia.Visuals/Media/GeometryDrawing.cs

@@ -0,0 +1,43 @@
+namespace Avalonia.Media
+{
+    public class GeometryDrawing : Drawing
+    {
+        public static readonly StyledProperty<Geometry> GeometryProperty =
+            AvaloniaProperty.Register<GeometryDrawing, Geometry>(nameof(Geometry));
+
+        public Geometry Geometry
+        {
+            get => GetValue(GeometryProperty);
+            set => SetValue(GeometryProperty, value);
+        }
+
+        public static readonly StyledProperty<IBrush> BrushProperty =
+            AvaloniaProperty.Register<GeometryDrawing, IBrush>(nameof(Brush), Brushes.Transparent);
+
+        public IBrush Brush
+        {
+            get => GetValue(BrushProperty);
+            set => SetValue(BrushProperty, value);
+        }
+
+        public static readonly StyledProperty<Pen> PenProperty =
+            AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen));
+
+        public Pen Pen
+        {
+            get => GetValue(PenProperty);
+            set => SetValue(PenProperty, value);
+        }
+
+        public override void Draw(DrawingContext context)
+        {
+            context.DrawGeometry(Brush, Pen, Geometry);
+        }
+
+        public override Rect GetBounds()
+        {
+            // adding the Pen's stroke thickness here could yield wrong results due to transforms
+            return Geometry?.GetRenderBounds(0) ?? new Rect();
+        }
+    }
+}

+ 67 - 13
src/Avalonia.Visuals/Media/LineGeometry.cs

@@ -10,35 +10,89 @@ namespace Avalonia.Media
     /// </summary>
     public class LineGeometry : Geometry
     {
-        private Point _startPoint;
-        private Point _endPoint;
+        /// <summary>
+        /// Defines the <see cref="StartPoint"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Point> StartPointProperty =
+            AvaloniaProperty.Register<LineGeometry, Point>(nameof(StartPoint));
+
+        public Point StartPoint
+        {
+            get => GetValue(StartPointProperty);
+            set => SetValue(StartPointProperty, value);
+        }
+
+        /// <summary>
+        /// Defines the <see cref="EndPoint"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Point> EndPointProperty =
+            AvaloniaProperty.Register<LineGeometry, Point>(nameof(EndPoint));
+        private bool _isDirty;
+
+        public Point EndPoint
+        {
+            get => GetValue(EndPointProperty);
+            set => SetValue(EndPointProperty, value);
+        }
+
+        static LineGeometry()
+        {
+            StartPointProperty.Changed.AddClassHandler<LineGeometry>(x => x.PointsChanged);
+            EndPointProperty.Changed.AddClassHandler<LineGeometry>(x => x.PointsChanged);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LineGeometry"/> class.
+        /// </summary>
+        public LineGeometry()
+        {
+            IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+            PlatformImpl = factory.CreateStreamGeometry();
+        }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LineGeometry"/> class.
         /// </summary>
         /// <param name="startPoint">The start point.</param>
         /// <param name="endPoint">The end point.</param>
-        public LineGeometry(Point startPoint, Point endPoint)
+        public LineGeometry(Point startPoint, Point endPoint) : this()
         {
-            _startPoint = startPoint;
-            _endPoint = endPoint;
-            IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            IStreamGeometryImpl impl = factory.CreateStreamGeometry();
+            StartPoint = startPoint;
+            EndPoint = endPoint;
+        }
 
-            using (IStreamGeometryContextImpl context = impl.Open())
+        public override IGeometryImpl PlatformImpl
+        {
+            get
             {
-                context.BeginFigure(_startPoint, false);
-                context.LineTo(_endPoint);
-                context.EndFigure(false);
+                PrepareIfNeeded();
+                return base.PlatformImpl;
             }
+            protected set => base.PlatformImpl = value;
+        }
 
-            PlatformImpl = impl;
+        public void PrepareIfNeeded()
+        {
+            if (_isDirty)
+            {
+                _isDirty = false;
+
+                using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
+                {
+                    context.BeginFigure(StartPoint, false);
+                    context.LineTo(EndPoint);
+                    context.EndFigure(false);
+                }
+            }
         }
 
         /// <inheritdoc/>
         public override Geometry Clone()
         {
-            return new LineGeometry(_startPoint, _endPoint);
+            PrepareIfNeeded();
+            return new LineGeometry(StartPoint, EndPoint);
         }
+
+        private void PointsChanged(AvaloniaPropertyChangedEventArgs e) => _isDirty = true;
     }
 }

+ 1 - 0
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@@ -141,6 +141,7 @@ namespace Avalonia.Media
                                 bool isLargeArc = ReadBool(reader);
                                 ReadSeparator(reader);
                                 SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
+                                ReadSeparator(reader);
                                 point = ReadPoint(reader, point, relative);
 
                                 _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);

+ 98 - 17
src/Avalonia.Visuals/Media/PolylineGeometry.cs

@@ -3,10 +3,9 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Avalonia.Platform;
+using Avalonia.Metadata;
+using Avalonia.Collections;
 
 namespace Avalonia.Media
 {
@@ -15,36 +14,118 @@ namespace Avalonia.Media
     /// </summary>
     public class PolylineGeometry : Geometry
     {
-        private IList<Point> _points;
-        private bool _isFilled;
+        /// <summary>
+        /// Defines the <see cref="Points"/> property.
+        /// </summary>
+        public static readonly DirectProperty<PolylineGeometry, Points> PointsProperty =
+            AvaloniaProperty.RegisterDirect<PolylineGeometry, Points>(nameof(Points), g => g.Points, (g, f) => g.Points = f);
 
-        public PolylineGeometry(IList<Point> points, bool isFilled)
+        /// <summary>
+        /// Defines the <see cref="IsFilled"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<bool> IsFilledProperty =
+            AvaloniaProperty.Register<PolylineGeometry, bool>(nameof(IsFilled));
+
+        private Points _points;
+        private bool _isDirty;
+        private IDisposable _pointsObserver;
+
+        static PolylineGeometry()
+        {
+            PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) =>
+                s.OnPointsChanged(e.OldValue as Points, e.NewValue as Points));
+            IsFilledProperty.Changed.AddClassHandler<PolylineGeometry>((s, _) => s.NotifyChanged());
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
+        /// </summary>
+        public PolylineGeometry()
         {
-            _points = points;
-            _isFilled = isFilled;
             IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            IStreamGeometryImpl impl = factory.CreateStreamGeometry();
+            PlatformImpl = factory.CreateStreamGeometry();
+
+            Points = new Points();
+        }
 
-            using (IStreamGeometryContextImpl context = impl.Open())
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
+        /// </summary>
+        public PolylineGeometry(IEnumerable<Point> points, bool isFilled) : this()
+        {
+            Points.AddRange(points);
+            IsFilled = isFilled;
+        }
+
+        public void PrepareIfNeeded()
+        {
+            if (_isDirty)
             {
-                if (points.Count > 0)
+                _isDirty = false;
+
+                using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
                 {
-                    context.BeginFigure(points[0], isFilled);
-                    for (int i = 1; i < points.Count; i++)
+                    var points = Points;
+                    var isFilled = IsFilled;
+                    if (points.Count > 0)
                     {
-                        context.LineTo(points[i]);
+                        context.BeginFigure(points[0], isFilled);
+                        for (int i = 1; i < points.Count; i++)
+                        {
+                            context.LineTo(points[i]);
+                        }
+                        context.EndFigure(isFilled);
                     }
-                    context.EndFigure(isFilled);
                 }
             }
+        }
 
-            PlatformImpl = impl;
+        /// <summary>
+        /// Gets or sets the figures.
+        /// </summary>
+        /// <value>
+        /// The points.
+        /// </value>
+        [Content]
+        public Points Points
+        {
+            get => _points;
+            set => SetAndRaise(PointsProperty, ref _points, value);
+        }
+
+        public bool IsFilled
+        {
+            get => GetValue(IsFilledProperty);
+            set => SetValue(IsFilledProperty, value);
+        }
+
+        public override IGeometryImpl PlatformImpl
+        {
+            get
+            {
+                PrepareIfNeeded();
+                return base.PlatformImpl;
+            }
+            protected set => base.PlatformImpl = value;
         }
 
         /// <inheritdoc/>
         public override Geometry Clone()
         {
-            return new PolylineGeometry(new List<Point>(_points), _isFilled);
+            PrepareIfNeeded();
+            return new PolylineGeometry(Points, IsFilled);
+        }
+
+        private void OnPointsChanged(Points oldValue, Points newValue)
+        {
+            _pointsObserver?.Dispose();
+
+            _pointsObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged());
+        }
+
+        internal void NotifyChanged()
+        {
+            _isDirty = true;
         }
     }
 }

+ 39 - 12
src/Avalonia.Visuals/Media/RectangleGeometry.cs

@@ -10,16 +10,51 @@ namespace Avalonia.Media
     /// </summary>
     public class RectangleGeometry : Geometry
     {
+        /// <summary>
+        /// Defines the <see cref="Rect"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Rect> RectProperty =
+            AvaloniaProperty.Register<RectangleGeometry, Rect>(nameof(Rect));
+
+        public Rect Rect
+        {
+            get => GetValue(RectProperty);
+            set => SetValue(RectProperty, value);
+        }
+
+        static RectangleGeometry()
+        {
+            RectProperty.Changed.AddClassHandler<RectangleGeometry>(x => x.RectChanged);
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="RectangleGeometry"/> class.
         /// </summary>
-        /// <param name="rect">The rectangle bounds.</param>
-        public RectangleGeometry(Rect rect)
+        public RectangleGeometry()
         {
             IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
-            IStreamGeometryImpl impl = factory.CreateStreamGeometry();
+            PlatformImpl = factory.CreateStreamGeometry();
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="RectangleGeometry"/> class.
+        /// </summary>
+        /// <param name="rect">The rectangle bounds.</param>
+        public RectangleGeometry(Rect rect) : this()
+        {
+            Rect = rect;
+        }
+
+        /// <inheritdoc/>
+        public override Geometry Clone()
+        {
+            return new RectangleGeometry(Rect);
+        }
 
-            using (IStreamGeometryContextImpl context = impl.Open())
+        private void RectChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            var rect = (Rect)e.NewValue;
+            using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
             {
                 context.BeginFigure(rect.TopLeft, true);
                 context.LineTo(rect.TopRight);
@@ -27,14 +62,6 @@ namespace Avalonia.Media
                 context.LineTo(rect.BottomLeft);
                 context.EndFigure(true);
             }
-
-            PlatformImpl = impl;
-        }
-
-        /// <inheritdoc/>
-        public override Geometry Clone()
-        {
-            return new RectangleGeometry(Bounds);
         }
     }
 }

+ 1 - 1
src/Avalonia.Visuals/Point.cs

@@ -183,7 +183,7 @@ namespace Avalonia
             }
             else
             {
-                throw new FormatException("Invalid Thickness.");
+                throw new FormatException("Invalid Point.");
             }
         }
 

+ 9 - 0
src/Avalonia.Visuals/Points.cs

@@ -0,0 +1,9 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Collections;
+
+namespace Avalonia
+{
+    public sealed class Points : AvaloniaList<Point> { }
+}

+ 26 - 0
src/Avalonia.Visuals/Rect.cs

@@ -481,5 +481,31 @@ namespace Avalonia
                 _width,
                 _height);
         }
+
+        /// <summary>
+        /// Parses a <see cref="Rect"/> string.
+        /// </summary>
+        /// <param name="s">The string.</param>
+        /// <param name="culture">The current culture.</param>
+        /// <returns>The parsed <see cref="Rect"/>.</returns>
+        public static Rect Parse(string s, CultureInfo culture)
+        {
+            var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(x => x.Trim())
+                .ToList();
+
+            if (parts.Count == 4)
+            {
+                return new Rect(
+                    double.Parse(parts[0], culture),
+                    double.Parse(parts[1], culture),
+                    double.Parse(parts[2], culture),
+                    double.Parse(parts[3], culture));
+            }
+            else
+            {
+                throw new FormatException("Invalid Rect.");
+            }
+        }
     }
 }

+ 1 - 1
src/Avalonia.Visuals/RelativeRect.cs

@@ -203,7 +203,7 @@ namespace Avalonia
             }
             else
             {
-                throw new FormatException("Invalid Rect.");
+                throw new FormatException("Invalid RelativeRect.");
             }
         }
     }

+ 24 - 18
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -11,6 +11,7 @@ using System.Collections.Generic;
 using System.IO;
 using Avalonia.Media.Immutable;
 using System.Threading;
+using System.Linq;
 
 namespace Avalonia.Rendering
 {
@@ -58,7 +59,6 @@ namespace Avalonia.Rendering
             _dispatcher = dispatcher ?? Dispatcher.UIThread;
             _root = root;
             _sceneBuilder = sceneBuilder ?? new SceneBuilder();
-            _scene = new Scene(root);
             _layerFactory = layerFactory ?? new DefaultRenderLayerFactory();
             _layers = new RenderLayers(_layerFactory);
             _renderLoop = renderLoop;
@@ -86,7 +86,6 @@ namespace Avalonia.Rendering
             _root = root;
             _renderTarget = renderTarget;
             _sceneBuilder = sceneBuilder ?? new SceneBuilder();
-            _scene = new Scene(root);
             _layerFactory = layerFactory ?? new DefaultRenderLayerFactory();
             _layers = new RenderLayers(_layerFactory);
         }
@@ -122,7 +121,7 @@ namespace Avalonia.Rendering
                 UpdateScene();
             }
 
-            return _scene.HitTest(p, filter);
+            return _scene?.HitTest(p, filter) ?? Enumerable.Empty<IVisual>();
         }
 
         /// <inheritdoc/>
@@ -186,7 +185,7 @@ namespace Avalonia.Rendering
                 _dirtyRectsDisplay.Tick();
             }
 
-            if (scene.Size != Size.Empty)
+            if (scene != null && scene.Size != Size.Empty)
             {
                 if (scene.Generation != _lastSceneId)
                 {
@@ -366,25 +365,32 @@ namespace Avalonia.Rendering
 
             try
             {
-                var scene = _scene.Clone();
-
-                if (_dirty == null)
-                {
-                    _dirty = new DirtyVisuals();
-                    _sceneBuilder.UpdateAll(scene);
-                }
-                else if (_dirty.Count > 0)
+                if (_root.IsVisible)
                 {
-                    foreach (var visual in _dirty)
+                    var scene = _scene?.Clone() ?? new Scene(_root);
+
+                    if (_dirty == null)
                     {
-                        _sceneBuilder.Update(scene, visual);
+                        _dirty = new DirtyVisuals();
+                        _sceneBuilder.UpdateAll(scene);
+                    }
+                    else if (_dirty.Count > 0)
+                    {
+                        foreach (var visual in _dirty)
+                        {
+                            _sceneBuilder.Update(scene, visual);
+                        }
                     }
-                }
 
-                Interlocked.Exchange(ref _scene, scene);
+                    Interlocked.Exchange(ref _scene, scene);
 
-                _dirty.Clear();
-                (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
+                    _dirty.Clear();
+                    (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
+                }
+                else
+                {
+                    Interlocked.Exchange(ref _scene, null);
+                }
             }
             finally
             {

+ 6 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -36,8 +36,14 @@ namespace Avalonia.Rendering.SceneGraph
         {
             Contract.Requires<ArgumentNullException>(scene != null);
             Contract.Requires<ArgumentNullException>(visual != null);
+
             Dispatcher.UIThread.VerifyAccess();
 
+            if (!scene.Root.Visual.IsVisible)
+            {
+                throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual.");
+            }
+
             var node = (VisualNode)scene.FindNode(visual);
 
             if (visual == scene.Root.Visual)

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -31,6 +31,8 @@
         </Compile>
         <Compile Include="AvaloniaXamlLoaderPortableXaml.cs" />
         <Compile Include="AvaloniaXamlLoader.cs" />
+        <Compile Include="Converters\MatrixTypeConverter.cs" />
+        <Compile Include="Converters\RectTypeConverter.cs" />
         <Compile Include="Converters\SetterValueTypeConverter.cs" />
         <Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
         <Compile Include="PortableXaml\AvaloniaXamlContext.cs" />

+ 23 - 0
src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs

@@ -0,0 +1,23 @@
+// 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.Globalization;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+	using System.ComponentModel;
+
+    public class MatrixTypeConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            return sourceType == typeof(string);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            return Matrix.Parse((string)value, culture);
+        }
+    }
+}

+ 23 - 0
src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs

@@ -0,0 +1,23 @@
+// 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.Globalization;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+	using System.ComponentModel;
+
+    public class RectTypeConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            return sourceType == typeof(string);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            return Rect.Parse((string)value, culture);
+        }
+    }
+}

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs

@@ -32,12 +32,14 @@ namespace Avalonia.Markup.Xaml.PortableXaml
             { typeof(AvaloniaList<double>), typeof(AvaloniaListTypeConverter<double>) },
             { typeof(IMemberSelector), typeof(MemberSelectorTypeConverter) },
             { typeof(Point), typeof(PointTypeConverter) },
+            { typeof(Matrix), typeof(MatrixTypeConverter) },
             { typeof(IList<Point>), typeof(PointsListTypeConverter) },
             { typeof(AvaloniaProperty), typeof(AvaloniaPropertyTypeConverter) },
             { typeof(RelativePoint), typeof(RelativePointTypeConverter) },
             { typeof(RelativeRect), typeof(RelativeRectTypeConverter) },
             { typeof(RowDefinitions), typeof(RowDefinitionsTypeConverter) },
             { typeof(Size), typeof(SizeTypeConverter) },
+            { typeof(Rect), typeof(RectTypeConverter) },
             { typeof(Selector), typeof(SelectorTypeConverter)},
             { typeof(SolidColorBrush), typeof(BrushTypeConverter) },
             { typeof(Thickness), typeof(ThicknessTypeConverter) },

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

@@ -119,7 +119,10 @@ namespace Avalonia.Direct2D1.Media
             using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size))
             using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_renderTarget.Factory, destRect.ToDirect2D()))
             {
-                d2dOpacityMask.PlatformBrush.Transform = Matrix.CreateTranslation(opacityMaskRect.Position).ToDirect2D();
+                if (d2dOpacityMask.PlatformBrush != null)
+                {
+                    d2dOpacityMask.PlatformBrush.Transform = Matrix.CreateTranslation(opacityMaskRect.Position).ToDirect2D();
+                }
 
                 _renderTarget.FillGeometry(
                     geometry,

+ 16 - 0
tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs

@@ -0,0 +1,16 @@
+using System.Globalization;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class MatrixTests
+    {
+        [Fact]
+        public void Parse_Parses()
+        {
+            var matrix = Matrix.Parse("1,2,3,-4,5 6", CultureInfo.CurrentCulture);
+            var expected = new Matrix(1, 2, 3, -4, 5, 6);
+            Assert.Equal(expected, matrix);
+        }
+    }
+}

+ 1 - 0
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@@ -56,6 +56,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         }
 
         [Theory]
+        [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107
         [InlineData("M0 0L10 10z")]
         [InlineData("M50 50 L100 100 L150 50")]
         [InlineData("M50 50L100 100L150 50")]

+ 16 - 0
tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs

@@ -0,0 +1,16 @@
+using System.Globalization;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class RectTests
+    {
+        [Fact]
+        public void Parse_Parses()
+        {
+            var rect = Rect.Parse("1,2 3,-4", CultureInfo.CurrentCulture);
+            var expected = new Rect(1, 2, 3, -4);
+            Assert.Equal(expected, rect);
+        }
+    }
+}

+ 0 - 27
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs

@@ -273,32 +273,5 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                     ((MockStreamGeometryImpl)borderLayer.GeometryClip).Transform);
             }
         }
-
-        [Fact]
-        public void Hiding_Root_Should_Not_Remove_Root_Layer()
-        {
-            using (TestApplication())
-            {
-                Border border;
-                var tree = new TestRoot
-                {
-                    Child = border = new Border()
-                };
-
-                var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
-                layout.ExecuteInitialLayoutPass(tree);
-
-                var scene = new Scene(tree);
-                var sceneBuilder = new SceneBuilder();
-                sceneBuilder.UpdateAll(scene);
-
-                tree.IsVisible = false;
-
-                scene = scene.Clone();
-                sceneBuilder.Update(scene, tree);
-
-                Assert.Equal(1, scene.Layers.Count);
-            }
-        }
     }
 }