diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs index 52edf12e7f..8c2c9a2c01 100644 --- a/src/Avalonia.Visuals/Media/GradientBrush.cs +++ b/src/Avalonia.Visuals/Media/GradientBrush.cs @@ -21,8 +21,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> GradientStopsProperty = - AvaloniaProperty.Register>(nameof(Opacity)); + public static readonly StyledProperty> GradientStopsProperty = + AvaloniaProperty.Register>(nameof(Opacity)); /// /// Initializes a new instance of the class. @@ -46,7 +46,7 @@ namespace Avalonia.Media /// Gets or sets the brush's gradient stops. /// [Content] - public IReadOnlyList GradientStops + public IList GradientStops { get { return GetValue(GradientStopsProperty); } set { SetValue(GradientStopsProperty, value); } diff --git a/src/Avalonia.Visuals/Media/IGradientBrush.cs b/src/Avalonia.Visuals/Media/IGradientBrush.cs index ce064c4a1f..390ce6ee5b 100644 --- a/src/Avalonia.Visuals/Media/IGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/IGradientBrush.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media /// /// Gets the brush's gradient stops. /// - IReadOnlyList GradientStops { get; } + IList GradientStops { get; } /// /// Gets the brush's spread method that defines how to draw a gradient that doesn't fill diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs index e8507f8fc3..ca67789c7f 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableGradientBrush.cs @@ -16,7 +16,7 @@ namespace Avalonia.Media.Immutable /// The opacity of the brush. /// The spread method. protected ImmutableGradientBrush( - IReadOnlyList gradientStops, + IList gradientStops, double opacity, GradientSpreadMethod spreadMethod) { @@ -36,7 +36,7 @@ namespace Avalonia.Media.Immutable } /// - public IReadOnlyList GradientStops { get; } + public IList GradientStops { get; } /// public double Opacity { get; } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs index b46ee951f7..39ddc305fb 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableLinearGradientBrush.cs @@ -20,7 +20,7 @@ namespace Avalonia.Media.Immutable /// The start point for the gradient. /// The end point for the gradient. public ImmutableLinearGradientBrush( - IReadOnlyList gradientStops, + IList gradientStops, double opacity = 1, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? startPoint = null, diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs index cc2c7b3697..672a45ebc4 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableRadialGradientBrush.cs @@ -22,7 +22,7 @@ namespace Avalonia.Media.Immutable /// The horizontal and vertical radius of the outermost circle of the radial gradient. /// public ImmutableRadialGradientBrush( - IReadOnlyList gradientStops, + IList gradientStops, double opacity = 1, GradientSpreadMethod spreadMethod = GradientSpreadMethod.Pad, RelativePoint? center = null, diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 145013d76b..fbc189546c 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -21,7 +21,10 @@ namespace Avalonia.Media { 'L', Command.Line }, { 'H', Command.HorizontalLine }, { 'V', Command.VerticalLine }, + { 'Q', Command.QuadraticBezierCurve }, + { 'T', Command.SmoothQuadraticBezierCurve }, { 'C', Command.CubicBezierCurve }, + { 'S', Command.SmoothCubicBezierCurve }, { 'A', Command.Arc }, { 'Z', Command.Close }, }; @@ -55,6 +58,9 @@ namespace Avalonia.Media HorizontalLine, VerticalLine, CubicBezierCurve, + QuadraticBezierCurve, + SmoothCubicBezierCurve, + SmoothQuadraticBezierCurve, Arc, Close, } @@ -71,7 +77,8 @@ namespace Avalonia.Media { Command command = Command.None; Point point = new Point(); - bool relative = false; + bool relative = false; + Point? previousControlPoint = null; while (ReadCommand(reader, ref command, ref relative)) { @@ -79,6 +86,7 @@ namespace Avalonia.Media { case Command.FillRule: _context.SetFillRule(ReadFillRule(reader)); + previousControlPoint = null; break; case Command.Move: @@ -90,11 +98,13 @@ namespace Avalonia.Media point = ReadPoint(reader, point, relative); _context.BeginFigure(point, true); openFigure = true; + previousControlPoint = null; break; case Command.Line: point = ReadPoint(reader, point, relative); _context.LineTo(point); + previousControlPoint = null; break; case Command.HorizontalLine: @@ -108,6 +118,7 @@ namespace Avalonia.Media } _context.LineTo(point); + previousControlPoint = null; break; case Command.VerticalLine: @@ -121,18 +132,57 @@ namespace Avalonia.Media } _context.LineTo(point); + previousControlPoint = null; break; + case Command.QuadraticBezierCurve: + { + Point handle = ReadPoint(reader, point, relative); + previousControlPoint = handle; + ReadSeparator(reader); + point = ReadPoint(reader, point, relative); + _context.QuadraticBezierTo(handle, point); + break; + } + + case Command.SmoothQuadraticBezierCurve: + { + Point end = ReadPoint(reader, point, relative); + + if(previousControlPoint != null) + previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); + + _context.QuadraticBezierTo(previousControlPoint ?? point, end); + point = end; + break; + } + case Command.CubicBezierCurve: { Point point1 = ReadPoint(reader, point, relative); ReadSeparator(reader); Point point2 = ReadPoint(reader, point, relative); + previousControlPoint = point2; ReadSeparator(reader); point = ReadPoint(reader, point, relative); _context.CubicBezierTo(point1, point2, point); break; } + + case Command.SmoothCubicBezierCurve: + { + Point point2 = ReadPoint(reader, point, relative); + ReadSeparator(reader); + Point end = ReadPoint(reader, point, relative); + + if(previousControlPoint != null) + previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point); + + _context.CubicBezierTo(previousControlPoint ?? point, point2, end); + previousControlPoint = point2; + point = end; + break; + } case Command.Arc: { @@ -147,12 +197,14 @@ namespace Avalonia.Media point = ReadPoint(reader, point, relative); _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); + previousControlPoint = null; break; } case Command.Close: _context.EndFigure(true); openFigure = false; + previousControlPoint = null; break; default: @@ -167,6 +219,12 @@ namespace Avalonia.Media } } + private Point MirrorControlPoint(Point controlPoint, Point center) + { + Point dir = (controlPoint - center); + return center + -dir; + } + private static bool ReadCommand( StringReader reader, ref Command command, @@ -243,6 +301,9 @@ namespace Avalonia.Media (c == 'E' && !readExponent) || char.IsDigit(c)) { + if (b.Length != 0 && !readExponent && c == '-') + break; + b.Append(c); reader.Read(); diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 5725ee2596..225aa2a795 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -342,7 +342,10 @@ namespace Avalonia.Gtk3 Native.GtkWindowResize(GtkWidget, (int)value.Width, (int)value.Height); } - public IScreenImpl Screen { get; } = new ScreenImpl(); + public IScreenImpl Screen + { + get; + } = new ScreenImpl(); public Point Position { diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index 71f008e074..9ce1756aae 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -153,7 +153,10 @@ namespace Avalonia.MonoMac Position = pos; } - public IScreenImpl Screen { get; } = new ScreenImpl(); + public IScreenImpl Screen + { + get; + } = new ScreenImpl(); public override Point PointToClient(Point point) { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index dab7f0ba04..4c28d44b93 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -103,7 +103,11 @@ namespace Avalonia.Win32 } } - public IScreenImpl Screen => new ScreenImpl(); + public IScreenImpl Screen + { + get; + } = new ScreenImpl(); + public IRenderer CreateRenderer(IRenderRoot root) { diff --git a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems index a3ca0c7493..ff729a6b48 100644 --- a/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems +++ b/tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems @@ -14,6 +14,7 @@ + diff --git a/tests/Avalonia.RenderTests/SVGPathTests.cs b/tests/Avalonia.RenderTests/SVGPathTests.cs new file mode 100644 index 0000000000..d1ed0ae1cf --- /dev/null +++ b/tests/Avalonia.RenderTests/SVGPathTests.cs @@ -0,0 +1,53 @@ +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +using System.Threading.Tasks; + +#if AVALONIA_CAIRO +namespace Avalonia.Cairo.RenderTests +#elif AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests +#endif +{ + public class SVGPathTests : TestBase + { + public SVGPathTests() + :base("SVGPath") + { + } + + [Fact] + public async Task SVGPath() + { + var target = new Canvas + { + Background = Brushes.Yellow, + Width = 76, + Height = 76, + Children = new Avalonia.Controls.Controls + { + new Path + { + Width = 32, + Height = 40, + [Canvas.LeftProperty] = 23, + [Canvas.TopProperty] = 18, + Stretch = Stretch.Fill, + Fill = Brushes.Black, + //Coffee Maker by Becris from the Noun Project + Data = StreamGeometry.Parse("M5,51v4c0,1.654,1.346,3,3,3h7v3c0,0.552,0.447,1,1,1h8c0.553,0,1-0.448,1-1v-3h18v3c0,0.552,0.447,1,1,1h8 c0.553,0,1-0.448,1-1v-3c2.757,0,5-2.243,5-5V13V7c0-2.757-2.243-5-5-5H11C8.243,2,6,4.243,6,7v2c0,2.757,2.243,5,5,5h1.743 l-2.717,11.775c-0.068,0.297,0.002,0.609,0.192,0.848C10.407,26.861,10.695,27,11,27h4c0.431,0,0.812-0.275,0.948-0.684L18.721,18 h1.499l1.811,7.243C22.142,25.688,22.541,26,23,26h12c0.459,0,0.858-0.312,0.97-0.757L37.78,18h6.658l-3.235,29.11 C41.147,47.618,40.72,48,40.21,48h-4.167c0.873-1.159,1.203-2.622,0.897-4.047L35,34.895v-2.481l2.707-2.707 c0.286-0.286,0.372-0.716,0.217-1.09C37.77,28.244,37.404,28,37,28H22c-0.553,0-1,0.448-1,1v0.719l-2.758-0.689 c-0.443-0.111-0.906,0.094-1.123,0.496l-7,13l1.762,0.948l6.631-12.315L21,31.781v3.115l-1.94,9.057 c-0.306,1.426,0.025,2.889,0.897,4.048H8C6.346,48,5,49.346,5,51z M23,60h-6v-2h6V60z M51,60h-6v-2h6V60z M8,9V7 c0-1.654,1.346-3,3-3h42c1.654,0,3,1.346,3,3v5H46H14h-3C9.346,12,8,10.654,8,9z M34.219,24H23.781l-1.5-6h13.438L34.219,24z M44.66,16H37H21h-3c-0.431,0-0.812,0.275-0.948,0.684L14.279,25h-2.022l2.539-11h30.087l-0.185,1.662L44.66,16z M43.191,47.331 L46.896,14H56v39c0,1.654-1.346,3-3,3h-1h-8H24h-8H8c-0.552,0-1-0.449-1-1v-4c0-0.551,0.448-1,1-1h15.948h8.104h8.158 C41.741,50,43.022,48.853,43.191,47.331z M23,30h11.586l-1.293,1.293C33.105,31.48,33,31.735,33,32v2H23V30z M21.614,46.886 c-0.571-0.708-0.79-1.624-0.6-2.514L22.809,36h10.383l1.794,8.372c0.19,0.89-0.028,1.806-0.6,2.514 C33.813,47.594,32.963,48,32.052,48h-8.104C23.037,48,22.187,47.594,21.614,46.886z") + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/TestFiles/Cairo/SVGPath/SVGPath.expected.png b/tests/TestFiles/Cairo/SVGPath/SVGPath.expected.png new file mode 100644 index 0000000000..9830048810 Binary files /dev/null and b/tests/TestFiles/Cairo/SVGPath/SVGPath.expected.png differ diff --git a/tests/TestFiles/Direct2D1/SVGPath/SVGPath.expected.png b/tests/TestFiles/Direct2D1/SVGPath/SVGPath.expected.png new file mode 100644 index 0000000000..9830048810 Binary files /dev/null and b/tests/TestFiles/Direct2D1/SVGPath/SVGPath.expected.png differ diff --git a/tests/TestFiles/Skia/SVGPath/SVGPath.expected.png b/tests/TestFiles/Skia/SVGPath/SVGPath.expected.png new file mode 100644 index 0000000000..9830048810 Binary files /dev/null and b/tests/TestFiles/Skia/SVGPath/SVGPath.expected.png differ