浏览代码

Merge pull request #2744 from AvaloniaUI/refactor/2709-mutable-pen

Make Pen mutable.
Steven Kirk 6 年之前
父节点
当前提交
f0a8e64189
共有 25 个文件被更改,包括 682 次插入116 次删除
  1. 21 14
      src/Avalonia.Visuals/Media/BrushExtensions.cs
  2. 85 43
      src/Avalonia.Visuals/Media/DashStyle.cs
  3. 4 4
      src/Avalonia.Visuals/Media/DrawingContext.cs
  4. 1 1
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  5. 20 0
      src/Avalonia.Visuals/Media/IDashStyle.cs
  6. 39 0
      src/Avalonia.Visuals/Media/IPen.cs
  7. 93 0
      src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
  8. 118 0
      src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
  9. 168 15
      src/Avalonia.Visuals/Media/Pen.cs
  10. 3 3
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  11. 2 2
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  12. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  13. 3 3
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  14. 1 1
      src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
  15. 5 4
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  16. 5 4
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  17. 5 4
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  18. 4 4
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 2 2
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  20. 3 3
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  21. 2 2
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  22. 2 2
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  23. 2 2
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  24. 91 0
      tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
  25. 2 2
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

+ 21 - 14
src/Avalonia.Visuals/Media/BrushExtensions.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using Avalonia.Media.Immutable;
 
 
 namespace Avalonia.Media
 namespace Avalonia.Media
 {
 {
@@ -23,27 +24,33 @@ namespace Avalonia.Media
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Converts a pen to a pen with an immutable brush
+        /// Converts a dash style to an immutable dash style.
+        /// </summary>
+        /// <param name="style">The dash style.</param>
+        /// <returns>
+        /// The result of calling <see cref="DashStyle.ToImmutable"/> if the style is mutable,
+        /// otherwise <paramref name="style"/>.
+        /// </returns>
+        public static ImmutableDashStyle ToImmutable(this IDashStyle style)
+        {
+            Contract.Requires<ArgumentNullException>(style != null);
+
+            return style as ImmutableDashStyle ?? ((DashStyle)style).ToImmutable();
+        }
+
+        /// <summary>
+        /// Converts a pen to an immutable pen.
         /// </summary>
         /// </summary>
         /// <param name="pen">The pen.</param>
         /// <param name="pen">The pen.</param>
         /// <returns>
         /// <returns>
-        /// A copy of the pen with an immutable brush, or <paramref name="pen"/> if the pen's brush
-        /// is already immutable or null.
+        /// The result of calling <see cref="Pen.ToImmutable"/> if the brush is mutable,
+        /// otherwise <paramref name="pen"/>.
         /// </returns>
         /// </returns>
-        public static Pen ToImmutable(this Pen pen)
+        public static ImmutablePen ToImmutable(this IPen pen)
         {
         {
             Contract.Requires<ArgumentNullException>(pen != null);
             Contract.Requires<ArgumentNullException>(pen != null);
 
 
-            var brush = pen.Brush?.ToImmutable();
-            return ReferenceEquals(pen.Brush, brush) ?
-                pen :
-                new Pen(
-                    brush,
-                    thickness: pen.Thickness,
-                    dashStyle: pen.DashStyle,                   
-                    lineCap: pen.LineCap,
-                    lineJoin: pen.LineJoin,
-                    miterLimit: pen.MiterLimit);
+            return pen as ImmutablePen ?? ((Pen)pen).ToImmutable();
         }
         }
     }
     }
 }
 }

+ 85 - 43
src/Avalonia.Visuals/Media/DashStyle.cs

@@ -1,72 +1,114 @@
 namespace Avalonia.Media
 namespace Avalonia.Media
 {
 {
+    using System;
     using System.Collections.Generic;
     using System.Collections.Generic;
+    using System.Linq;
     using Avalonia.Animation;
     using Avalonia.Animation;
+    using Avalonia.Media.Immutable;
 
 
-    public class DashStyle : Animatable
+    /// <summary>
+    /// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
+    /// </summary>
+    public class DashStyle : Animatable, IDashStyle, IAffectsRender
     {
     {
-        private static DashStyle dash;
-        public static DashStyle Dash
-        {
-            get
-            {
-                if (dashDotDot == null)
-                {
-                    dash = new DashStyle(new double[] { 2, 2 }, 1);
-                }
-
-                return dash;
-            }
-        }
+        /// <summary>
+        /// Defines the <see cref="Dashes"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<IReadOnlyList<double>> DashesProperty =
+            AvaloniaProperty.Register<DashStyle, IReadOnlyList<double>>(nameof(Dashes));
 
 
+        /// <summary>
+        /// Defines the <see cref="Offset"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<double> OffsetProperty =
+            AvaloniaProperty.Register<DashStyle, double>(nameof(Offset));
 
 
+        private static ImmutableDashStyle s_dash;
+        private static ImmutableDashStyle s_dot;
+        private static ImmutableDashStyle s_dashDot;
+        private static ImmutableDashStyle s_dashDotDot;
 
 
-        private static DashStyle dot;
-        public static DashStyle Dot
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DashStyle"/> class.
+        /// </summary>
+        public DashStyle()
+            : this(null, 0)
         {
         {
-            get { return dot ?? (dot = new DashStyle(new double[] {0, 2}, 0)); }
         }
         }
 
 
-        private static DashStyle dashDot;
-        public static DashStyle DashDot
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DashStyle"/> class.
+        /// </summary>
+        /// <param name="dashes">The dashes collection.</param>
+        /// <param name="offset">The dash sequence offset.</param>
+        public DashStyle(IEnumerable<double> dashes, double offset)
         {
         {
-            get
-            {
-                if (dashDot == null)
-                {
-                    dashDot = new DashStyle(new double[] { 2, 2, 0, 2 }, 1);
-                }
-
-                return dashDot;
-            }
+            Dashes = (IReadOnlyList<double>)dashes?.ToList() ?? Array.Empty<double>();
+            Offset = offset;
         }
         }
 
 
-        private static DashStyle dashDotDot;
-        public static DashStyle DashDotDot
+        static DashStyle()
         {
         {
-            get
+            void RaiseInvalidated(AvaloniaPropertyChangedEventArgs e)
             {
             {
-                if (dashDotDot == null)
-                {
-                    dashDotDot = new DashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1);
-                }
-
-                return dashDotDot;
+                ((DashStyle)e.Sender).Invalidated?.Invoke(e.Sender, EventArgs.Empty);
             }
             }
+
+            DashesProperty.Changed.Subscribe(RaiseInvalidated);
+            OffsetProperty.Changed.Subscribe(RaiseInvalidated);
         }
         }
 
 
+        /// <summary>
+        /// Represents a dashed <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle Dash =>
+            s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1));
+
+        /// <summary>
+        /// Represents a dotted <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle Dot =>
+            s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0));
+
+        /// <summary>
+        /// Represents a dashed dotted <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle DashDot =>
+            s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1));
+
+        /// <summary>
+        /// Represents a dashed double dotted <see cref="DashStyle"/>.
+        /// </summary>
+        public static IDashStyle DashDotDot =>
+            s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1));
 
 
-        public DashStyle(IReadOnlyList<double> dashes = null, double offset = 0.0)
+        /// <summary>
+        /// Gets or sets the length of alternating dashes and gaps.
+        /// </summary>
+        public IReadOnlyList<double> Dashes
         {
         {
-            this.Dashes = dashes;
-            this.Offset = offset;
+            get => GetValue(DashesProperty);
+            set => SetValue(DashesProperty, value);
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Gets and sets the length of alternating dashes and gaps.
+        /// Gets or sets how far in the dash sequence the stroke will start.
         /// </summary>
         /// </summary>
-        public IReadOnlyList<double> Dashes { get; }
+        public double Offset
+        {
+            get => GetValue(OffsetProperty);
+            set => SetValue(OffsetProperty, value);
+        }
 
 
-        public double Offset { get; }
+        /// <summary>
+        /// Raised when the dash style changes.
+        /// </summary>
+        public event EventHandler Invalidated;
+
+        /// <summary>
+        /// Returns an immutable clone of the <see cref="DashStyle"/>.
+        /// </summary>
+        /// <returns></returns>
+        public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset);
     }
     }
 }
 }

+ 4 - 4
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -94,7 +94,7 @@ namespace Avalonia.Media
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
-        public void DrawLine(Pen pen, Point p1, Point p2)
+        public void DrawLine(IPen pen, Point p1, Point p2)
         {
         {
             if (PenIsVisible(pen))
             if (PenIsVisible(pen))
             {
             {
@@ -108,7 +108,7 @@ namespace Avalonia.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="geometry">The geometry.</param>
-        public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry)
+        public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry)
         {
         {
             Contract.Requires<ArgumentNullException>(geometry != null);
             Contract.Requires<ArgumentNullException>(geometry != null);
 
 
@@ -124,7 +124,7 @@ namespace Avalonia.Media
         /// <param name="pen">The pen.</param>
         /// <param name="pen">The pen.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         /// <param name="cornerRadius">The corner radius.</param>
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f)
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f)
         {
         {
             if (PenIsVisible(pen))
             if (PenIsVisible(pen))
             {
             {
@@ -328,7 +328,7 @@ namespace Avalonia.Media
                 PlatformImpl.Dispose();
                 PlatformImpl.Dispose();
         }
         }
 
 
-        private static bool PenIsVisible(Pen pen)
+        private static bool PenIsVisible(IPen pen)
         {
         {
             return pen?.Brush != null && pen.Thickness > 0;
             return pen?.Brush != null && pen.Thickness > 0;
         }
         }

+ 1 - 1
src/Avalonia.Visuals/Media/GeometryDrawing.cs

@@ -23,7 +23,7 @@
         public static readonly StyledProperty<Pen> PenProperty =
         public static readonly StyledProperty<Pen> PenProperty =
             AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen));
             AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen));
 
 
-        public Pen Pen
+        public IPen Pen
         {
         {
             get => GetValue(PenProperty);
             get => GetValue(PenProperty);
             set => SetValue(PenProperty, value);
             set => SetValue(PenProperty, value);

+ 20 - 0
src/Avalonia.Visuals/Media/IDashStyle.cs

@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
+    /// </summary>
+    public interface IDashStyle
+    {
+        /// <summary>
+        /// Gets or sets the length of alternating dashes and gaps.
+        /// </summary>
+        IReadOnlyList<double> Dashes { get; }
+
+        /// <summary>
+        /// Gets or sets how far in the dash sequence the stroke will start.
+        /// </summary>
+        double Offset { get; }
+    }
+}

+ 39 - 0
src/Avalonia.Visuals/Media/IPen.cs

@@ -0,0 +1,39 @@
+namespace Avalonia.Media
+{
+    /// <summary>
+    /// Describes how a stroke is drawn.
+    /// </summary>
+    public interface IPen
+    {
+        /// <summary>
+        /// Gets the brush used to draw the stroke.
+        /// </summary>
+        IBrush Brush { get; }
+
+        /// <summary>
+        /// Gets the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
+        IDashStyle DashStyle { get; }
+
+        /// <summary>
+        /// Gets the type of shape to use on both ends of a line.
+        /// </summary>
+        PenLineCap LineCap { get; }
+
+        /// <summary>
+        /// Gets a value describing how to join consecutive line or curve segments in a 
+        /// <see cref="PathFigure"/> contained in a <see cref="PathGeometry"/> object.
+        /// </summary>
+        PenLineJoin LineJoin { get; }
+
+        /// <summary>
+        /// Gets the limit of the thickness of the join on a mitered corner.
+        /// </summary>
+        double MiterLimit { get; }
+
+        /// <summary>
+        /// Gets the stroke thickness.
+        /// </summary>
+        double Thickness { get; }
+    }
+}

+ 93 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs

@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Media.Immutable
+{
+    /// <summary>
+    /// Represents the sequence of dashes and gaps that will be applied by an
+    /// <see cref="ImmutablePen"/>.
+    /// </summary>
+    public class ImmutableDashStyle : IDashStyle, IEquatable<IDashStyle>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ImmutableDashStyle"/> class.
+        /// </summary>
+        /// <param name="dashes">The dashes collection.</param>
+        /// <param name="offset">The dash sequence offset.</param>
+        public ImmutableDashStyle(IEnumerable<double> dashes, double offset)
+        {
+            Dashes = (IReadOnlyList<double>)dashes?.ToList() ?? Array.Empty<double>();
+            Offset = offset;
+        }
+
+        /// <inheritdoc/>
+        public IReadOnlyList<double> Dashes { get; }
+
+        /// <inheritdoc/>
+        public double Offset { get; }
+
+        /// <inheritdoc/>
+        public override bool Equals(object obj) => Equals(obj as IDashStyle);
+
+        /// <inheritdoc/>
+        public bool Equals(IDashStyle other)
+        {
+            if (ReferenceEquals(this, other))
+            {
+                return true;
+            }
+            else if (other is null)
+            {
+                return false;
+            }
+
+            if (Offset != other.Offset)
+            {
+                return false;
+            }
+
+            return SequenceEqual(Dashes, other.Dashes);
+        }
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            var hashCode = 717868523;
+            hashCode = hashCode * -1521134295 + Offset.GetHashCode();
+
+            if (Dashes != null)
+            {
+                foreach (var i in Dashes)
+                {
+                    hashCode = hashCode * -1521134295 + i.GetHashCode();
+                }
+            }
+
+            return hashCode;
+        }
+
+        private static bool SequenceEqual(IReadOnlyList<double> left, IReadOnlyList<double> right)
+        {
+            if (left == right)
+            {
+                return true;
+            }
+
+            if (left == null || right == null || left.Count != right.Count)
+            {
+                return false;
+            }
+
+            for (var c = 0; c < left.Count; c++)
+            {
+                if (left[c] != right[c])
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+}

+ 118 - 0
src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs

@@ -0,0 +1,118 @@
+// 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.Collections.Generic;
+
+namespace Avalonia.Media.Immutable
+{
+    /// <summary>
+    /// Describes how a stroke is drawn.
+    /// </summary>
+    public class ImmutablePen : IPen, IEquatable<IPen>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Pen"/> class.
+        /// </summary>
+        /// <param name="color">The stroke color.</param>
+        /// <param name="thickness">The stroke thickness.</param>
+        /// <param name="dashStyle">The dash style.</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 ImmutablePen(
+            uint color,
+            double thickness = 1.0,
+            ImmutableDashStyle dashStyle = null,
+            PenLineCap lineCap = PenLineCap.Flat,
+            PenLineJoin lineJoin = PenLineJoin.Miter,
+            double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Pen"/> class.
+        /// </summary>
+        /// <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="lineCap">The line cap.</param>
+        /// <param name="lineJoin">The line join.</param>
+        /// <param name="miterLimit">The miter limit.</param>
+        public ImmutablePen(
+            IBrush brush,
+            double thickness = 1.0,
+            ImmutableDashStyle dashStyle = null,
+            PenLineCap lineCap = PenLineCap.Flat,
+            PenLineJoin lineJoin = PenLineJoin.Miter,
+            double miterLimit = 10.0)
+        {
+            Brush = brush;
+            Thickness = thickness;
+            LineCap = lineCap;
+            LineJoin = lineJoin;
+            MiterLimit = miterLimit;
+            DashStyle = dashStyle;
+        }
+
+        /// <summary>
+        /// Gets the brush used to draw the stroke.
+        /// </summary>
+        public IBrush Brush { get; }
+
+        /// <summary>
+        /// Gets the stroke thickness.
+        /// </summary>
+        public double Thickness { get; }
+
+        /// <summary>
+        /// Specifies the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
+        public IDashStyle DashStyle { get; }
+
+        /// <summary>
+        /// Specifies the type of graphic shape to use on both ends of a line.
+        /// </summary>
+        public PenLineCap LineCap { get; }
+
+        /// <summary>
+        /// Specifies how to join consecutive line or curve segments in a <see cref="PathFigure"/>
+        /// (subpaths) contained in a <see cref="PathGeometry"/> object.
+        /// </summary>
+        public PenLineJoin LineJoin { get; }
+
+        /// <summary>
+        /// The limit on the ratio of the miter length to half this pen's Thickness.
+        /// </summary>
+        public double MiterLimit { get; }
+
+        /// <inheritdoc/>
+        public override bool Equals(object obj) => Equals(obj as IPen);
+
+        /// <inheritdoc/>
+        public bool Equals(IPen other)
+        {
+            if (ReferenceEquals(this, other))
+            {
+                return true;
+            }
+            else if (other is null)
+            {
+                return false;
+            }
+
+            return EqualityComparer<IBrush>.Default.Equals(Brush, other.Brush) &&
+               Thickness == other.Thickness &&
+               EqualityComparer<IDashStyle>.Default.Equals(DashStyle, other.DashStyle) &&
+               LineCap == other.LineCap &&
+               LineJoin == other.LineJoin &&
+               MiterLimit == other.MiterLimit;
+        }
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            return (Brush, Thickness, DashStyle, LineCap, LineJoin, MiterLimit).GetHashCode();
+        }
+    }
+}

+ 168 - 15
src/Avalonia.Visuals/Media/Pen.cs

@@ -1,13 +1,61 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // 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.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 
+using System;
+using System.Collections.Generic;
+using Avalonia.Media.Immutable;
+using Avalonia.Utilities;
+
 namespace Avalonia.Media
 namespace Avalonia.Media
 {
 {
     /// <summary>
     /// <summary>
     /// Describes how a stroke is drawn.
     /// Describes how a stroke is drawn.
     /// </summary>
     /// </summary>
-    public class Pen
+    public class Pen : AvaloniaObject, IPen
     {
     {
+        /// <summary>
+        /// Defines the <see cref="Brush"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IBrush> BrushProperty =
+            AvaloniaProperty.Register<Pen, IBrush>(nameof(Brush));
+
+        /// <summary>
+        /// Defines the <see cref="Thickness"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> ThicknessProperty =
+            AvaloniaProperty.Register<Pen, double>(nameof(Thickness), 1.0);
+
+        /// <summary>
+        /// Defines the <see cref="DashStyle"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IDashStyle> DashStyleProperty =
+            AvaloniaProperty.Register<Pen, IDashStyle>(nameof(DashStyle));
+
+        /// <summary>
+        /// Defines the <see cref="LineCap"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PenLineCap> LineCapProperty =
+            AvaloniaProperty.Register<Pen, PenLineCap>(nameof(LineCap));
+
+        /// <summary>
+        /// Defines the <see cref="LineJoin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PenLineJoin> LineJoinProperty =
+            AvaloniaProperty.Register<Pen, PenLineJoin>(nameof(LineJoin));
+
+        /// <summary>
+        /// Defines the <see cref="MiterLimit"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MiterLimitProperty =
+            AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0);
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Pen"/> class.
+        /// </summary>
+        public Pen()
+        {
+        }
+
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="Pen"/> class.
         /// Initializes a new instance of the <see cref="Pen"/> class.
         /// </summary>
         /// </summary>
@@ -20,7 +68,7 @@ namespace Avalonia.Media
         public Pen(
         public Pen(
             uint color,
             uint color,
             double thickness = 1.0,
             double thickness = 1.0,
-            DashStyle dashStyle = null,
+            IDashStyle dashStyle = null,
             PenLineCap lineCap = PenLineCap.Flat,
             PenLineCap lineCap = PenLineCap.Flat,
             PenLineJoin lineJoin = PenLineJoin.Miter,
             PenLineJoin lineJoin = PenLineJoin.Miter,
             double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
             double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
@@ -39,7 +87,7 @@ namespace Avalonia.Media
         public Pen(
         public Pen(
             IBrush brush,
             IBrush brush,
             double thickness = 1.0,
             double thickness = 1.0,
-            DashStyle dashStyle = null,
+            IDashStyle dashStyle = null,
             PenLineCap lineCap = PenLineCap.Flat,
             PenLineCap lineCap = PenLineCap.Flat,
             PenLineJoin lineJoin = PenLineJoin.Miter,
             PenLineJoin lineJoin = PenLineJoin.Miter,
             double miterLimit = 10.0)
             double miterLimit = 10.0)
@@ -52,34 +100,139 @@ namespace Avalonia.Media
             DashStyle = dashStyle;
             DashStyle = dashStyle;
         }
         }
 
 
+        static Pen()
+        {
+            AffectsRender<Pen>(
+                BrushProperty,
+                ThicknessProperty,
+                DashStyleProperty,
+                LineCapProperty,
+                LineJoinProperty,
+                MiterLimitProperty);
+        }
+
+        /// <summary>
+        /// Gets or sets the brush used to draw the stroke.
+        /// </summary>
+        public IBrush Brush
+        {
+            get => GetValue(BrushProperty);
+            set => SetValue(BrushProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the stroke thickness.
+        /// </summary>
+        public double Thickness
+        {
+            get => GetValue(ThicknessProperty);
+            set => SetValue(ThicknessProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// </summary>
+        public IDashStyle DashStyle
+        {
+            get => GetValue(DashStyleProperty);
+            set => SetValue(DashStyleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the type of shape to use on both ends of a line.
+        /// </summary>
+        public PenLineCap LineCap
+        {
+            get => GetValue(LineCapProperty);
+            set => SetValue(LineCapProperty, value);
+        }
+
         /// <summary>
         /// <summary>
-        /// Gets the brush used to draw the stroke.
+        /// Gets or sets the join style for the ends of two consecutive lines drawn with this
+        /// <see cref="Pen"/>.
         /// </summary>
         /// </summary>
-        public IBrush Brush { get; }
+        public PenLineJoin LineJoin
+        {
+            get => GetValue(LineJoinProperty);
+            set => SetValue(LineJoinProperty, value);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Gets the stroke thickness.
+        /// Gets or sets the limit of the thickness of the join on a mitered corner.
         /// </summary>
         /// </summary>
-        public double Thickness { get; }
+        public double MiterLimit
+        {
+            get => GetValue(MiterLimitProperty);
+            set => SetValue(MiterLimitProperty, value);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Specifies the style of dashed lines drawn with a <see cref="Pen"/> object.
+        /// Raised when the pen changes.
         /// </summary>
         /// </summary>
-        public DashStyle DashStyle { get; }
+        public event EventHandler Invalidated;
 
 
         /// <summary>
         /// <summary>
-        /// Specifies the type of graphic shape to use on both ends of a line.
+        /// Creates an immutable clone of the brush.
         /// </summary>
         /// </summary>
-        public PenLineCap LineCap { get; }
+        /// <returns>The immutable clone.</returns>
+        public ImmutablePen ToImmutable()
+        {
+            return new ImmutablePen(
+                Brush?.ToImmutable(),
+                Thickness,
+                DashStyle?.ToImmutable(),
+                LineCap,
+                LineJoin,
+                MiterLimit);
+        }
 
 
         /// <summary>
         /// <summary>
-        /// Specifies how to join consecutive line or curve segments in a <see cref="PathFigure"/> (subpath) contained in a <see cref="PathGeometry"/> object.
+        /// Marks a property as affecting the pen's visual representation.
         /// </summary>
         /// </summary>
-        public PenLineJoin LineJoin { get; }
+        /// <param name="properties">The properties.</param>
+        /// <remarks>
+        /// After a call to this method in a pen's static constructor, any change to the
+        /// property will cause the <see cref="Invalidated"/> event to be raised on the pen.
+        /// </remarks>
+        protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
+            where T : Pen
+        {
+            void Invalidate(AvaloniaPropertyChangedEventArgs e)
+            {
+                if (e.Sender is T sender)
+                {
+                    if (e.OldValue is IAffectsRender oldValue)
+                    {
+                        WeakEventHandlerManager.Unsubscribe<EventArgs, T>(
+                            oldValue,
+                            nameof(oldValue.Invalidated),
+                            sender.AffectsRenderInvalidated);
+                    }
+
+                    if (e.NewValue is IAffectsRender newValue)
+                    {
+                        WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(
+                            newValue,
+                            nameof(newValue.Invalidated),
+                            sender.AffectsRenderInvalidated);
+                    }
+
+                    sender.RaiseInvalidated(EventArgs.Empty);
+                }
+            }
+
+            foreach (var property in properties)
+            {
+                property.Changed.Subscribe(Invalidate);
+            }
+        }
 
 
         /// <summary>
         /// <summary>
-        /// The limit on the ratio of the miter length to half this pen's Thickness.
+        /// Raises the <see cref="Invalidated"/> event.
         /// </summary>
         /// </summary>
-        public double MiterLimit { get; }
+        /// <param name="e">The event args.</param>
+        protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
+
+        private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty);
     }
     }
 }
 }

+ 3 - 3
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@@ -50,7 +50,7 @@ namespace Avalonia.Platform
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
-        void DrawLine(Pen pen, Point p1, Point p2);
+        void DrawLine(IPen pen, Point p1, Point p2);
 
 
         /// <summary>
         /// <summary>
         /// Draws a geometry.
         /// Draws a geometry.
@@ -58,7 +58,7 @@ namespace Avalonia.Platform
         /// <param name="brush">The fill brush.</param>
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="geometry">The geometry.</param>
-        void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry);
+        void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
 
 
         /// <summary>
         /// <summary>
         /// Draws the outline of a rectangle.
         /// Draws the outline of a rectangle.
@@ -66,7 +66,7 @@ namespace Avalonia.Platform
         /// <param name="pen">The pen.</param>
         /// <param name="pen">The pen.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         /// <param name="cornerRadius">The corner radius.</param>
-        void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f);
+        void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f);
 
 
         /// <summary>
         /// <summary>
         /// Draws text.
         /// Draws text.

+ 2 - 2
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@@ -20,7 +20,7 @@ namespace Avalonia.Platform
         /// </summary>
         /// </summary>
         /// <param name="pen">The pen to use. May be null.</param>
         /// <param name="pen">The pen to use. May be null.</param>
         /// <returns>The bounding rectangle.</returns>
         /// <returns>The bounding rectangle.</returns>
-        Rect GetRenderBounds(Pen pen);
+        Rect GetRenderBounds(IPen pen);
 
 
         /// <summary>
         /// <summary>
         /// Indicates whether the geometry's fill contains the specified point.
         /// Indicates whether the geometry's fill contains the specified point.
@@ -42,7 +42,7 @@ namespace Avalonia.Platform
         /// <param name="pen">The stroke to use.</param>
         /// <param name="pen">The stroke to use.</param>
         /// <param name="point">The point.</param>
         /// <param name="point">The point.</param>
         /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
-        bool StrokeContains(Pen pen, Point point);
+        bool StrokeContains(IPen pen, Point point);
 
 
         /// <summary>
         /// <summary>
         /// Makes a clone of the geometry with the specified transform.
         /// Makes a clone of the geometry with the specified transform.

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     /// </summary>
     internal abstract class BrushDrawOperation : DrawOperation
     internal abstract class BrushDrawOperation : DrawOperation
     {
     {
-        public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen)
+        public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen)
             : base(bounds, transform, pen)
             : base(bounds, transform, pen)
         {
         {
         }
         }

+ 3 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
         {
             var next = NextDrawAs<GeometryNode>();
             var next = NextDrawAs<GeometryNode>();
 
 
@@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public void DrawLine(Pen pen, Point p1, Point p2)
+        public void DrawLine(IPen pen, Point p1, Point p2)
         {
         {
             var next = NextDrawAs<LineNode>();
             var next = NextDrawAs<LineNode>();
 
 
@@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
         {
         {
             var next = NextDrawAs<RectangleNode>();
             var next = NextDrawAs<RectangleNode>();
 
 

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     /// </summary>
     internal abstract class DrawOperation : IDrawOperation
     internal abstract class DrawOperation : IDrawOperation
     {
     {
-        public DrawOperation(Rect bounds, Matrix transform, Pen pen)
+        public DrawOperation(Rect bounds, Matrix transform, IPen pen)
         {
         {
             bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform);
             bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform);
             Bounds = new Rect(
             Bounds = new Rect(

+ 5 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -3,6 +3,7 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
 
 
@@ -24,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
         public GeometryNode(
         public GeometryNode(
             Matrix transform,
             Matrix transform,
             IBrush brush,
             IBrush brush,
-            Pen pen,
+            IPen pen,
             IGeometryImpl geometry,
             IGeometryImpl geometry,
             IDictionary<IVisual, Scene> childScenes = null)
             IDictionary<IVisual, Scene> childScenes = null)
             : base(geometry.GetRenderBounds(pen), transform, null)
             : base(geometry.GetRenderBounds(pen), transform, null)
@@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// <summary>
         /// Gets the stroke pen.
         /// Gets the stroke pen.
         /// </summary>
         /// </summary>
-        public Pen Pen { get; }
+        public ImmutablePen Pen { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the geometry to draw.
         /// Gets the geometry to draw.
@@ -71,11 +72,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
         /// </remarks>
-        public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry)
         {
         {
             return transform == Transform &&
             return transform == Transform &&
                 Equals(brush, Brush) && 
                 Equals(brush, Brush) && 
-                pen == Pen &&
+                Equals(Pen, pen) &&
                 Equals(geometry, Geometry);
                 Equals(geometry, Geometry);
         }
         }
 
 

+ 5 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@@ -3,6 +3,7 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
 
 
@@ -23,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
         /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
         public LineNode(
         public LineNode(
             Matrix transform,
             Matrix transform,
-            Pen pen,
+            IPen pen,
             Point p1,
             Point p1,
             Point p2,
             Point p2,
             IDictionary<IVisual, Scene> childScenes = null)
             IDictionary<IVisual, Scene> childScenes = null)
@@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// <summary>
         /// Gets the stroke pen.
         /// Gets the stroke pen.
         /// </summary>
         /// </summary>
-        public Pen Pen { get; }
+        public ImmutablePen Pen { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the start point of the line.
         /// Gets the start point of the line.
@@ -71,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
         /// </remarks>
-        public bool Equals(Matrix transform, Pen pen, Point p1, Point p2)
+        public bool Equals(Matrix transform, IPen pen, Point p1, Point p2)
         {
         {
-            return transform == Transform && pen == Pen && p1 == P1 && p2 == P2;
+            return transform == Transform && Equals(Pen, pen) && p1 == P1 && p2 == P2;
         }
         }
 
 
         public override void Render(IDrawingContextImpl context)
         public override void Render(IDrawingContextImpl context)

+ 5 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -3,6 +3,7 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
 
 
@@ -25,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph
         public RectangleNode(
         public RectangleNode(
             Matrix transform,
             Matrix transform,
             IBrush brush,
             IBrush brush,
-            Pen pen,
+            IPen pen,
             Rect rect,
             Rect rect,
             float cornerRadius,
             float cornerRadius,
             IDictionary<IVisual, Scene> childScenes = null)
             IDictionary<IVisual, Scene> childScenes = null)
@@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <summary>
         /// <summary>
         /// Gets the stroke pen.
         /// Gets the stroke pen.
         /// </summary>
         /// </summary>
-        public Pen Pen { get; }
+        public ImmutablePen Pen { get; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the rectangle to draw.
         /// Gets the rectangle to draw.
@@ -80,11 +81,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
         /// </remarks>
-        public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius)
+        public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, float cornerRadius)
         {
         {
             return transform == Transform &&
             return transform == Transform &&
                 Equals(brush, Brush) &&
                 Equals(brush, Brush) &&
-                pen == Pen &&
+                Equals(Pen, pen) &&
                 rect == Rect &&
                 rect == Rect &&
                 cornerRadius == CornerRadius;
                 cornerRadius == CornerRadius;
         }
         }

+ 4 - 4
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -154,7 +154,7 @@ namespace Avalonia.Skia
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void DrawLine(Pen pen, Point p1, Point p2)
+        public void DrawLine(IPen pen, Point p1, Point p2)
         {
         {
             using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
             using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
             {
             {
@@ -163,7 +163,7 @@ namespace Avalonia.Skia
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
         {
             var impl = (GeometryImpl) geometry;
             var impl = (GeometryImpl) geometry;
             var size = geometry.Bounds.Size;
             var size = geometry.Bounds.Size;
@@ -184,7 +184,7 @@ namespace Avalonia.Skia
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0)
         {
         {
             using (var paint = CreatePaint(pen, rect.Size))
             using (var paint = CreatePaint(pen, rect.Size))
             {
             {
@@ -561,7 +561,7 @@ namespace Avalonia.Skia
         /// <param name="pen">Source pen.</param>
         /// <param name="pen">Source pen.</param>
         /// <param name="targetSize">Target size.</param>
         /// <param name="targetSize">Target size.</param>
         /// <returns></returns>
         /// <returns></returns>
-        private PaintWrapper CreatePaint(Pen pen, Size targetSize)
+        private PaintWrapper CreatePaint(IPen pen, Size targetSize)
         {
         {
             // In Skia 0 thickness means - use hairline rendering
             // In Skia 0 thickness means - use hairline rendering
             // and for us it means - there is nothing rendered.
             // and for us it means - there is nothing rendered.

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

@@ -26,7 +26,7 @@ namespace Avalonia.Skia
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public bool StrokeContains(Pen pen, Point point)
+        public bool StrokeContains(IPen pen, Point point)
         {
         {
             // Skia requires to compute stroke path to check for point containment.
             // Skia requires to compute stroke path to check for point containment.
             // Due to that we are caching using stroke width.
             // Due to that we are caching using stroke width.
@@ -89,7 +89,7 @@ namespace Avalonia.Skia
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public Rect GetRenderBounds(Pen pen)
+        public Rect GetRenderBounds(IPen pen)
         {
         {
             var strokeWidth = (float)(pen?.Thickness ?? 0);
             var strokeWidth = (float)(pen?.Thickness ?? 0);
             
             

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

@@ -174,7 +174,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p1">The first point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
         /// <param name="p2">The second point of the line.</param>
-        public void DrawLine(Pen pen, Point p1, Point p2)
+        public void DrawLine(IPen pen, Point p1, Point p2)
         {
         {
             if (pen != null)
             if (pen != null)
             {
             {
@@ -202,7 +202,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="brush">The fill brush.</param>
         /// <param name="brush">The fill brush.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="pen">The stroke pen.</param>
         /// <param name="geometry">The geometry.</param>
         /// <param name="geometry">The geometry.</param>
-        public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
+        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
         {
         {
             if (brush != null)
             if (brush != null)
             {
             {
@@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media
         /// <param name="pen">The pen.</param>
         /// <param name="pen">The pen.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="rect">The rectangle bounds.</param>
         /// <param name="cornerRadius">The corner radius.</param>
         /// <param name="cornerRadius">The corner radius.</param>
-        public void DrawRectangle(Pen pen, Rect rect, float cornerRadius)
+        public void DrawRectangle(IPen pen, Rect rect, float cornerRadius)
         {
         {
             using (var brush = CreateBrush(pen.Brush, rect.Size))
             using (var brush = CreateBrush(pen.Brush, rect.Size))
             using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
             using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))

+ 2 - 2
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media
         public Geometry Geometry { get; }
         public Geometry Geometry { get; }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public Rect GetRenderBounds(Avalonia.Media.Pen pen)
+        public Rect GetRenderBounds(Avalonia.Media.IPen pen)
         {
         {
             return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
             return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
         }
         }
@@ -46,7 +46,7 @@ namespace Avalonia.Direct2D1.Media
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
+        public bool StrokeContains(Avalonia.Media.IPen pen, Point point)
         {
         {
             return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0));
             return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0));
         }
         }

+ 2 - 2
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1
         /// <param name="pen">The pen to convert.</param>
         /// <param name="pen">The pen to convert.</param>
         /// <param name="renderTarget">The render target.</param>
         /// <param name="renderTarget">The render target.</param>
         /// <returns>The Direct2D brush.</returns>
         /// <returns>The Direct2D brush.</returns>
-        public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
+        public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
         {
         {
             return pen.ToDirect2DStrokeStyle(renderTarget.Factory);
             return pen.ToDirect2DStrokeStyle(renderTarget.Factory);
         }
         }
@@ -120,7 +120,7 @@ namespace Avalonia.Direct2D1
         /// <param name="pen">The pen to convert.</param>
         /// <param name="pen">The pen to convert.</param>
         /// <param name="factory">The factory associated with this resource.</param>
         /// <param name="factory">The factory associated with this resource.</param>
         /// <returns>The Direct2D brush.</returns>
         /// <returns>The Direct2D brush.</returns>
-        public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
+        public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.IPen pen, Factory factory)
         {
         {
             var d2dLineCap = pen.LineCap.ToDirect2D();
             var d2dLineCap = pen.LineCap.ToDirect2D();
 
 

+ 2 - 2
tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs

@@ -47,12 +47,12 @@ namespace Avalonia.UnitTests
             return _context.FillContains(point);
             return _context.FillContains(point);
         }
         }
 
 
-        public bool StrokeContains(Pen pen, Point point)
+        public bool StrokeContains(IPen pen, Point point)
         {
         {
             return false;
             return false;
         }
         }
 
 
-        public Rect GetRenderBounds(Pen pen) => Bounds;
+        public Rect GetRenderBounds(IPen pen) => Bounds;
 
 
         public IGeometryImpl Intersect(IGeometryImpl geometry)
         public IGeometryImpl Intersect(IGeometryImpl geometry)
         {
         {

+ 91 - 0
tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs

@@ -0,0 +1,91 @@
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class PenTests
+    {
+        [Fact]
+        public void Changing_Thickness_Raises_Invalidated()
+        {
+            var target = new Pen();
+            var raised = false;
+
+            target.Invalidated += (s, e) => raised = true;
+            target.Thickness = 18;
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void Changing_Brush_Color_Raises_Invalidated()
+        {
+            var brush = new SolidColorBrush(Colors.Red);
+            var target = new Pen { Brush = brush };
+            var raised = false;
+
+            target.Invalidated += (s, e) => raised = true;
+            brush.Color = Colors.Green;
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void Changing_DashStyle_Dashes_Raises_Invalidated()
+        {
+            var dashes = new DashStyle();
+            var target = new Pen { DashStyle = dashes };
+            var raised = false;
+
+            target.Invalidated += (s, e) => raised = true;
+            dashes.Dashes = new[] { 0.1, 0.2 };
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void Equality_Is_Implemented_Between_Immutable_And_Mmutable_Pens()
+        {
+            var brush = new SolidColorBrush(Colors.Red);
+            var target1 = new ImmutablePen(
+                brush: brush,
+                thickness: 2,
+                dashStyle: (ImmutableDashStyle)DashStyle.Dash,
+                lineCap: PenLineCap.Round,
+                lineJoin: PenLineJoin.Round,
+                miterLimit: 21);
+            var target2 = new Pen(
+                brush: brush,
+                thickness: 2,
+                dashStyle: DashStyle.Dash,
+                lineCap: PenLineCap.Round,
+                lineJoin: PenLineJoin.Round,
+                miterLimit: 21);
+
+            Assert.True(Equals(target1, target2));
+        }
+
+        [Fact]
+        public void Equality_Is_Implemented_Between_Mutable_And_Immutable_DashStyles()
+        {
+            var brush = new SolidColorBrush(Colors.Red);
+            var target1 = new ImmutablePen(
+                brush: brush,
+                thickness: 2,
+                dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
+                lineCap: PenLineCap.Round,
+                lineJoin: PenLineJoin.Round,
+                miterLimit: 21);
+            var target2 = new Pen(
+                brush: brush,
+                thickness: 2,
+                dashStyle: new DashStyle(new[] { 0.1, 0.2 }, 5),
+                lineCap: PenLineCap.Round,
+                lineJoin: PenLineJoin.Round,
+                miterLimit: 21);
+
+            Assert.True(Equals(target1, target2));
+        }
+    }
+}

+ 2 - 2
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@@ -96,7 +96,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                 return _impl.FillContains(point);
                 return _impl.FillContains(point);
             }
             }
 
 
-            public Rect GetRenderBounds(Pen pen)
+            public Rect GetRenderBounds(IPen pen)
             {
             {
                 throw new NotImplementedException();
                 throw new NotImplementedException();
             }
             }
@@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
                 return _impl;
                 return _impl;
             }
             }
 
 
-            public bool StrokeContains(Pen pen, Point point)
+            public bool StrokeContains(IPen pen, Point point)
             {
             {
                 throw new NotImplementedException();
                 throw new NotImplementedException();
             }
             }