Browse Source

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

Make Pen mutable.
pull/2797/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
f0a8e64189
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      src/Avalonia.Visuals/Media/BrushExtensions.cs
  2. 128
      src/Avalonia.Visuals/Media/DashStyle.cs
  3. 8
      src/Avalonia.Visuals/Media/DrawingContext.cs
  4. 2
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  5. 20
      src/Avalonia.Visuals/Media/IDashStyle.cs
  6. 39
      src/Avalonia.Visuals/Media/IPen.cs
  7. 93
      src/Avalonia.Visuals/Media/Immutable/ImmutableDashStyle.cs
  8. 118
      src/Avalonia.Visuals/Media/Immutable/ImmutablePen.cs
  9. 183
      src/Avalonia.Visuals/Media/Pen.cs
  10. 6
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  11. 4
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  12. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  13. 6
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  14. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs
  15. 9
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  16. 9
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  17. 9
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  18. 8
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 4
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  20. 6
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  21. 4
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  22. 4
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  23. 4
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  24. 91
      tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs
  25. 4
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

35
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
}
/// <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>
/// <param name="pen">The pen.</param>
/// <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>
public static Pen ToImmutable(this Pen pen)
public static ImmutablePen ToImmutable(this IPen pen)
{
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();
}
}
}

128
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
/// <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>
/// Gets and sets the length of alternating dashes and gaps.
/// Gets or sets how far in the dash sequence the stroke will start.
/// </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);
}
}

8
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -94,7 +94,7 @@ namespace Avalonia.Media
/// <param name="pen">The stroke pen.</param>
/// <param name="p1">The first 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))
{
@ -108,7 +108,7 @@ namespace Avalonia.Media
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</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);
@ -124,7 +124,7 @@ namespace Avalonia.Media
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</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))
{
@ -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;
}

2
src/Avalonia.Visuals/Media/GeometryDrawing.cs

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

20
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
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
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
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();
}
}
}

183
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
{
/// <summary>
/// Describes how a stroke is drawn.
/// </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>
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
@ -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<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>
/// 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>
public IBrush Brush { get; }
public PenLineJoin LineJoin
{
get => GetValue(LineJoinProperty);
set => SetValue(LineJoinProperty, value);
}
/// <summary>
/// Gets the stroke thickness.
/// Gets or sets the limit of the thickness of the join on a mitered corner.
/// </summary>
public double Thickness { get; }
public double MiterLimit
{
get => GetValue(MiterLimitProperty);
set => SetValue(MiterLimitProperty, value);
}
/// <summary>
/// Specifies the style of dashed lines drawn with a <see cref="Pen"/> object.
/// Raised when the pen changes.
/// </summary>
public DashStyle DashStyle { get; }
public event EventHandler Invalidated;
/// <summary>
/// Specifies the type of graphic shape to use on both ends of a line.
/// Creates an immutable clone of the brush.
/// </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>
/// 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>
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>
/// The limit on the ratio of the miter length to half this pen's Thickness.
/// Raises the <see cref="Invalidated"/> event.
/// </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);
}
}

6
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -50,7 +50,7 @@ namespace Avalonia.Platform
/// <param name="pen">The stroke pen.</param>
/// <param name="p1">The first 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>
/// Draws a geometry.
@ -58,7 +58,7 @@ namespace Avalonia.Platform
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry);
void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry);
/// <summary>
/// Draws the outline of a rectangle.
@ -66,7 +66,7 @@ namespace Avalonia.Platform
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</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>
/// Draws text.

4
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@ -20,7 +20,7 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="pen">The pen to use. May be null.</param>
/// <returns>The bounding rectangle.</returns>
Rect GetRenderBounds(Pen pen);
Rect GetRenderBounds(IPen pen);
/// <summary>
/// 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="point">The point.</param>
/// <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>
/// Makes a clone of the geometry with the specified transform.

2
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
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)
{
}

6
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -100,7 +100,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
@ -137,7 +137,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void DrawLine(Pen pen, Point p1, Point p2)
public void DrawLine(IPen pen, Point p1, Point p2)
{
var next = NextDrawAs<LineNode>();
@ -152,7 +152,7 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <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>();

2
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
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(

9
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<IVisual, Scene> childScenes = null)
: base(geometry.GetRenderBounds(pen), transform, null)
@ -49,7 +50,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Gets the stroke pen.
/// </summary>
public Pen Pen { get; }
public ImmutablePen Pen { get; }
/// <summary>
/// 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.
/// </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 &&
Equals(brush, Brush) &&
pen == Pen &&
Equals(Pen, pen) &&
Equals(geometry, Geometry);
}

9
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
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public LineNode(
Matrix transform,
Pen pen,
IPen pen,
Point p1,
Point p2,
IDictionary<IVisual, Scene> childScenes = null)
@ -44,7 +45,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Gets the stroke pen.
/// </summary>
public Pen Pen { get; }
public ImmutablePen Pen { get; }
/// <summary>
/// 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.
/// </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)

9
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<IVisual, Scene> childScenes = null)
@ -52,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Gets the stroke pen.
/// </summary>
public Pen Pen { get; }
public ImmutablePen Pen { get; }
/// <summary>
/// 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.
/// </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 &&
Equals(brush, Brush) &&
pen == Pen &&
Equals(Pen, pen) &&
rect == Rect &&
cornerRadius == CornerRadius;
}

8
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -154,7 +154,7 @@ namespace Avalonia.Skia
}
/// <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))))
{
@ -163,7 +163,7 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
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
}
/// <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))
{
@ -561,7 +561,7 @@ namespace Avalonia.Skia
/// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param>
/// <returns></returns>
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.

4
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -26,7 +26,7 @@ namespace Avalonia.Skia
}
/// <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.
// Due to that we are caching using stroke width.
@ -89,7 +89,7 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public Rect GetRenderBounds(Pen pen)
public Rect GetRenderBounds(IPen pen)
{
var strokeWidth = (float)(pen?.Thickness ?? 0);

6
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -174,7 +174,7 @@ namespace Avalonia.Direct2D1.Media
/// <param name="pen">The stroke pen.</param>
/// <param name="p1">The first 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)
{
@ -202,7 +202,7 @@ namespace Avalonia.Direct2D1.Media
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</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)
{
@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</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 d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))

4
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1.Media
public Geometry Geometry { get; }
/// <inheritdoc/>
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
}
/// <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));
}

4
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@ -109,7 +109,7 @@ namespace Avalonia.Direct2D1
/// <param name="pen">The pen to convert.</param>
/// <param name="renderTarget">The render target.</param>
/// <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);
}
@ -120,7 +120,7 @@ namespace Avalonia.Direct2D1
/// <param name="pen">The pen to convert.</param>
/// <param name="factory">The factory associated with this resource.</param>
/// <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();

4
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)
{

91
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));
}
}
}

4
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();
}

Loading…
Cancel
Save