diff --git a/src/Avalonia.Visuals/Media/BrushExtensions.cs b/src/Avalonia.Visuals/Media/BrushExtensions.cs index 522953eb04..87e698e705 100644 --- a/src/Avalonia.Visuals/Media/BrushExtensions.cs +++ b/src/Avalonia.Visuals/Media/BrushExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.Immutable; namespace Avalonia.Media { @@ -23,27 +24,33 @@ namespace Avalonia.Media } /// - /// Converts a pen to a pen with an immutable brush + /// Converts a dash style to an immutable dash style. + /// + /// The dash style. + /// + /// The result of calling if the style is mutable, + /// otherwise . + /// + public static ImmutableDashStyle ToImmutable(this IDashStyle style) + { + Contract.Requires(style != null); + + return style as ImmutableDashStyle ?? ((DashStyle)style).ToImmutable(); + } + + /// + /// Converts a pen to an immutable pen. /// /// The pen. /// - /// A copy of the pen with an immutable brush, or if the pen's brush - /// is already immutable or null. + /// The result of calling if the brush is mutable, + /// otherwise . /// - public static Pen ToImmutable(this Pen pen) + public static ImmutablePen ToImmutable(this IPen pen) { Contract.Requires(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(); } } } diff --git a/src/Avalonia.Visuals/Media/DashStyle.cs b/src/Avalonia.Visuals/Media/DashStyle.cs index c7e1db57b2..7784c73736 100644 --- a/src/Avalonia.Visuals/Media/DashStyle.cs +++ b/src/Avalonia.Visuals/Media/DashStyle.cs @@ -1,72 +1,114 @@ namespace Avalonia.Media { + using System; using System.Collections.Generic; + using System.Linq; using Avalonia.Animation; + using Avalonia.Media.Immutable; - public class DashStyle : Animatable + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + 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; - } - } + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty> DashesProperty = + AvaloniaProperty.Register>(nameof(Dashes)); + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty OffsetProperty = + AvaloniaProperty.Register(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 + /// + /// Initializes a new instance of the class. + /// + public DashStyle() + : this(null, 0) { - get { return dot ?? (dot = new DashStyle(new double[] {0, 2}, 0)); } } - private static DashStyle dashDot; - public static DashStyle DashDot + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public DashStyle(IEnumerable dashes, double offset) { - get - { - if (dashDot == null) - { - dashDot = new DashStyle(new double[] { 2, 2, 0, 2 }, 1); - } - - return dashDot; - } + Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + 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); } + /// + /// Represents a dashed . + /// + public static IDashStyle Dash => + s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1)); + + /// + /// Represents a dotted . + /// + public static IDashStyle Dot => + s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0)); + + /// + /// Represents a dashed dotted . + /// + public static IDashStyle DashDot => + s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1)); + + /// + /// Represents a dashed double dotted . + /// + public static IDashStyle DashDotDot => + s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1)); - public DashStyle(IReadOnlyList dashes = null, double offset = 0.0) + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + public IReadOnlyList Dashes { - this.Dashes = dashes; - this.Offset = offset; + get => GetValue(DashesProperty); + set => SetValue(DashesProperty, value); } /// - /// Gets and sets the length of alternating dashes and gaps. + /// Gets or sets how far in the dash sequence the stroke will start. /// - public IReadOnlyList Dashes { get; } + public double Offset + { + get => GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } - public double Offset { get; } + /// + /// Raised when the dash style changes. + /// + public event EventHandler Invalidated; + + /// + /// Returns an immutable clone of the . + /// + /// + public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset); } } diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index d3af71ffcb..4c9bf9ebd4 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -94,7 +94,7 @@ namespace Avalonia.Media /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) { @@ -108,7 +108,7 @@ namespace Avalonia.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) + public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry) { Contract.Requires(geometry != null); @@ -124,7 +124,7 @@ namespace Avalonia.Media /// The pen. /// The rectangle bounds. /// The corner radius. - 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)) { @@ -328,7 +328,7 @@ namespace Avalonia.Media PlatformImpl.Dispose(); } - private static bool PenIsVisible(Pen pen) + private static bool PenIsVisible(IPen pen) { return pen?.Brush != null && pen.Thickness > 0; } diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index ac0cc1c17d..3dad10fb8f 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -23,7 +23,7 @@ public static readonly StyledProperty PenProperty = AvaloniaProperty.Register(nameof(Pen)); - public Pen Pen + public IPen Pen { get => GetValue(PenProperty); set => SetValue(PenProperty, value); diff --git a/src/Avalonia.Visuals/Media/IDashStyle.cs b/src/Avalonia.Visuals/Media/IDashStyle.cs new file mode 100644 index 0000000000..7835c7a1e9 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IDashStyle.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Avalonia.Media +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by a . + /// + public interface IDashStyle + { + /// + /// Gets or sets the length of alternating dashes and gaps. + /// + IReadOnlyList Dashes { get; } + + /// + /// Gets or sets how far in the dash sequence the stroke will start. + /// + double Offset { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IPen.cs b/src/Avalonia.Visuals/Media/IPen.cs new file mode 100644 index 0000000000..0cdac312cc --- /dev/null +++ b/src/Avalonia.Visuals/Media/IPen.cs @@ -0,0 +1,39 @@ +namespace Avalonia.Media +{ + /// + /// Describes how a stroke is drawn. + /// + public interface IPen + { + /// + /// Gets the brush used to draw the stroke. + /// + IBrush Brush { get; } + + /// + /// Gets the style of dashed lines drawn with a object. + /// + IDashStyle DashStyle { get; } + + /// + /// Gets the type of shape to use on both ends of a line. + /// + PenLineCap LineCap { get; } + + /// + /// Gets a value describing how to join consecutive line or curve segments in a + /// contained in a object. + /// + PenLineJoin LineJoin { get; } + + /// + /// Gets the limit of the thickness of the join on a mitered corner. + /// + double MiterLimit { get; } + + /// + /// Gets the stroke thickness. + /// + double Thickness { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs new file mode 100644 index 0000000000..e9a52fe6ed --- /dev/null +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Media.Immutable +{ + /// + /// Represents the sequence of dashes and gaps that will be applied by an + /// . + /// + public class ImmutableDashStyle : IDashStyle, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The dashes collection. + /// The dash sequence offset. + public ImmutableDashStyle(IEnumerable dashes, double offset) + { + Dashes = (IReadOnlyList)dashes?.ToList() ?? Array.Empty(); + Offset = offset; + } + + /// + public IReadOnlyList Dashes { get; } + + /// + public double Offset { get; } + + /// + public override bool Equals(object obj) => Equals(obj as IDashStyle); + + /// + 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); + } + + /// + 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 left, IReadOnlyList 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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs new file mode 100644 index 0000000000..4b3bd640cb --- /dev/null +++ b/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 +{ + /// + /// Describes how a stroke is drawn. + /// + public class ImmutablePen : IPen, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The stroke color. + /// The stroke thickness. + /// The dash style. + /// Specifies the type of graphic shape to use on both ends of a line. + /// The line join. + /// The miter limit. + 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) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The brush used to draw. + /// The stroke thickness. + /// The dash style. + /// The line cap. + /// The line join. + /// The miter limit. + 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; + } + + /// + /// Gets the brush used to draw the stroke. + /// + public IBrush Brush { get; } + + /// + /// Gets the stroke thickness. + /// + public double Thickness { get; } + + /// + /// Specifies the style of dashed lines drawn with a object. + /// + public IDashStyle DashStyle { get; } + + /// + /// Specifies the type of graphic shape to use on both ends of a line. + /// + public PenLineCap LineCap { get; } + + /// + /// Specifies how to join consecutive line or curve segments in a + /// (subpaths) contained in a object. + /// + public PenLineJoin LineJoin { get; } + + /// + /// The limit on the ratio of the miter length to half this pen's Thickness. + /// + public double MiterLimit { get; } + + /// + public override bool Equals(object obj) => Equals(obj as IPen); + + /// + public bool Equals(IPen other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + else if (other is null) + { + return false; + } + + return EqualityComparer.Default.Equals(Brush, other.Brush) && + Thickness == other.Thickness && + EqualityComparer.Default.Equals(DashStyle, other.DashStyle) && + LineCap == other.LineCap && + LineJoin == other.LineJoin && + MiterLimit == other.MiterLimit; + } + + /// + public override int GetHashCode() + { + return (Brush, Thickness, DashStyle, LineCap, LineJoin, MiterLimit).GetHashCode(); + } + } +} diff --git a/src/Avalonia.Visuals/Media/Pen.cs b/src/Avalonia.Visuals/Media/Pen.cs index ee427c913b..b88fae28ff 100644 --- a/src/Avalonia.Visuals/Media/Pen.cs +++ b/src/Avalonia.Visuals/Media/Pen.cs @@ -1,13 +1,61 @@ // 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; +using Avalonia.Media.Immutable; +using Avalonia.Utilities; + namespace Avalonia.Media { /// /// Describes how a stroke is drawn. /// - public class Pen + public class Pen : AvaloniaObject, IPen { + /// + /// Defines the property. + /// + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ThicknessProperty = + AvaloniaProperty.Register(nameof(Thickness), 1.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DashStyleProperty = + AvaloniaProperty.Register(nameof(DashStyle)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineCapProperty = + AvaloniaProperty.Register(nameof(LineCap)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty LineJoinProperty = + AvaloniaProperty.Register(nameof(LineJoin)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MiterLimitProperty = + AvaloniaProperty.Register(nameof(MiterLimit), 10.0); + + /// + /// Initializes a new instance of the class. + /// + public Pen() + { + } + /// /// Initializes a new instance of the class. /// @@ -20,7 +68,7 @@ namespace Avalonia.Media public Pen( uint color, double thickness = 1.0, - DashStyle dashStyle = null, + IDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit) @@ -39,7 +87,7 @@ namespace Avalonia.Media public Pen( IBrush brush, double thickness = 1.0, - DashStyle dashStyle = null, + IDashStyle dashStyle = null, PenLineCap lineCap = PenLineCap.Flat, PenLineJoin lineJoin = PenLineJoin.Miter, double miterLimit = 10.0) @@ -52,34 +100,139 @@ namespace Avalonia.Media DashStyle = dashStyle; } + static Pen() + { + AffectsRender( + BrushProperty, + ThicknessProperty, + DashStyleProperty, + LineCapProperty, + LineJoinProperty, + MiterLimitProperty); + } + + /// + /// Gets or sets the brush used to draw the stroke. + /// + public IBrush Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + /// + /// Gets or sets the stroke thickness. + /// + public double Thickness + { + get => GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); + } + + /// + /// Gets or sets the style of dashed lines drawn with a object. + /// + public IDashStyle DashStyle + { + get => GetValue(DashStyleProperty); + set => SetValue(DashStyleProperty, value); + } + + /// + /// Gets or sets the type of shape to use on both ends of a line. + /// + public PenLineCap LineCap + { + get => GetValue(LineCapProperty); + set => SetValue(LineCapProperty, value); + } + /// - /// Gets the brush used to draw the stroke. + /// Gets or sets the join style for the ends of two consecutive lines drawn with this + /// . /// - public IBrush Brush { get; } + public PenLineJoin LineJoin + { + get => GetValue(LineJoinProperty); + set => SetValue(LineJoinProperty, value); + } /// - /// Gets the stroke thickness. + /// Gets or sets the limit of the thickness of the join on a mitered corner. /// - public double Thickness { get; } + public double MiterLimit + { + get => GetValue(MiterLimitProperty); + set => SetValue(MiterLimitProperty, value); + } /// - /// Specifies the style of dashed lines drawn with a object. + /// Raised when the pen changes. /// - public DashStyle DashStyle { get; } + public event EventHandler Invalidated; /// - /// Specifies the type of graphic shape to use on both ends of a line. + /// Creates an immutable clone of the brush. /// - public PenLineCap LineCap { get; } + /// The immutable clone. + public ImmutablePen ToImmutable() + { + return new ImmutablePen( + Brush?.ToImmutable(), + Thickness, + DashStyle?.ToImmutable(), + LineCap, + LineJoin, + MiterLimit); + } /// - /// Specifies how to join consecutive line or curve segments in a (subpath) contained in a object. + /// Marks a property as affecting the pen's visual representation. /// - public PenLineJoin LineJoin { get; } + /// The properties. + /// + /// After a call to this method in a pen's static constructor, any change to the + /// property will cause the event to be raised on the pen. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : Pen + { + void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is T sender) + { + if (e.OldValue is IAffectsRender oldValue) + { + WeakEventHandlerManager.Unsubscribe( + oldValue, + nameof(oldValue.Invalidated), + sender.AffectsRenderInvalidated); + } + + if (e.NewValue is IAffectsRender newValue) + { + WeakEventHandlerManager.Subscribe( + newValue, + nameof(newValue.Invalidated), + sender.AffectsRenderInvalidated); + } + + sender.RaiseInvalidated(EventArgs.Empty); + } + } + + foreach (var property in properties) + { + property.Changed.Subscribe(Invalidate); + } + } /// - /// The limit on the ratio of the miter length to half this pen's Thickness. + /// Raises the event. /// - public double MiterLimit { get; } + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + private void AffectsRenderInvalidated(object sender, EventArgs e) => RaiseInvalidated(EventArgs.Empty); } } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index e5be04ebf9..f74c551fe0 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Platform /// The stroke pen. /// The first point of the line. /// The second point of the line. - void DrawLine(Pen pen, Point p1, Point p2); + void DrawLine(IPen pen, Point p1, Point p2); /// /// Draws a geometry. @@ -58,7 +58,7 @@ namespace Avalonia.Platform /// The fill brush. /// The stroke pen. /// The geometry. - void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry); + void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry); /// /// Draws the outline of a rectangle. @@ -66,7 +66,7 @@ namespace Avalonia.Platform /// The pen. /// The rectangle bounds. /// The corner radius. - void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f); + void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f); /// /// Draws text. diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index 4e8e6521bd..b762859d1d 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.Platform /// /// The pen to use. May be null. /// The bounding rectangle. - Rect GetRenderBounds(Pen pen); + Rect GetRenderBounds(IPen pen); /// /// Indicates whether the geometry's fill contains the specified point. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(Pen pen, Point point); + bool StrokeContains(IPen pen, Point point); /// /// Makes a clone of the geometry with the specified transform. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index 4c09dc2ddd..b2c0581388 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// 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) { } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 0b33851911..3af56f5215 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var next = NextDrawAs(); @@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { var next = NextDrawAs(); @@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs index 1a5a6fad3f..d9dfd8bd55 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph /// 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 = new Rect( diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 2d01b117d9..d5aa1251f3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -24,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph public GeometryNode( Matrix transform, IBrush brush, - Pen pen, + IPen pen, IGeometryImpl geometry, IDictionary childScenes = null) : base(geometry.GetRenderBounds(pen), transform, null) @@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// 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 /// allocation of a not-yet-constructed draw operation object. /// - 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 && Equals(brush, Brush) && - pen == Pen && + Equals(Pen, pen) && Equals(geometry, Geometry); } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 11c763fcc9..9a65fac078 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -23,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph /// Child scenes for drawing visual brushes. public LineNode( Matrix transform, - Pen pen, + IPen pen, Point p1, Point p2, IDictionary childScenes = null) @@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// 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 /// allocation of a not-yet-constructed draw operation object. /// - 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) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index c622dc8a43..0f3581b84c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.VisualTree; @@ -25,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph public RectangleNode( Matrix transform, IBrush brush, - Pen pen, + IPen pen, Rect rect, float cornerRadius, IDictionary childScenes = null) @@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the stroke pen. /// - public Pen Pen { get; } + public ImmutablePen Pen { get; } /// /// 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 /// allocation of a not-yet-constructed draw operation object. /// - 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 && Equals(brush, Brush) && - pen == Pen && + Equals(Pen, pen) && rect == Rect && cornerRadius == CornerRadius; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 262d87d8b6..47e651ce91 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -154,7 +154,7 @@ namespace Avalonia.Skia } /// - 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)))) { @@ -163,7 +163,7 @@ namespace Avalonia.Skia } /// - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; @@ -184,7 +184,7 @@ namespace Avalonia.Skia } /// - 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)) { @@ -561,7 +561,7 @@ namespace Avalonia.Skia /// Source pen. /// Target size. /// - private PaintWrapper CreatePaint(Pen pen, Size targetSize) + private PaintWrapper CreatePaint(IPen pen, Size targetSize) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 5940de418e..23980fb913 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.Skia } /// - 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. // Due to that we are caching using stroke width. @@ -89,7 +89,7 @@ namespace Avalonia.Skia } /// - public Rect GetRenderBounds(Pen pen) + public Rect GetRenderBounds(IPen pen) { var strokeWidth = (float)(pen?.Thickness ?? 0); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e90d444c44..39d801eb2f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -174,7 +174,7 @@ namespace Avalonia.Direct2D1.Media /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) + public void DrawLine(IPen pen, Point p1, Point p2) { if (pen != null) { @@ -202,7 +202,7 @@ namespace Avalonia.Direct2D1.Media /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { if (brush != null) { @@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media /// The pen. /// The rectangle bounds. /// The corner radius. - 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 d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 7c8ddaca3f..51ca2520ad 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media public Geometry Geometry { get; } /// - public Rect GetRenderBounds(Avalonia.Media.Pen pen) + public Rect GetRenderBounds(Avalonia.Media.IPen pen) { return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia(); } @@ -46,7 +46,7 @@ namespace Avalonia.Direct2D1.Media } /// - 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)); } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 6b0d30f250..065895859d 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The render target. /// The Direct2D brush. - 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); } @@ -120,7 +120,7 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The factory associated with this resource. /// The Direct2D brush. - 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(); diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs index 63da9ed3f0..4fa3fbf523 100644 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs @@ -47,12 +47,12 @@ namespace Avalonia.UnitTests return _context.FillContains(point); } - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { return false; } - public Rect GetRenderBounds(Pen pen) => Bounds; + public Rect GetRenderBounds(IPen pen) => Bounds; public IGeometryImpl Intersect(IGeometryImpl geometry) { diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs new file mode 100644 index 0000000000..418ac7576b --- /dev/null +++ b/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)); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 03470670d2..d31210bc71 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -96,7 +96,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl.FillContains(point); } - public Rect GetRenderBounds(Pen pen) + public Rect GetRenderBounds(IPen pen) { throw new NotImplementedException(); } @@ -111,7 +111,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl; } - public bool StrokeContains(Pen pen, Point point) + public bool StrokeContains(IPen pen, Point point) { throw new NotImplementedException(); }