From 7f8dc7766ea500a1046763e73e0640d62973660c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 12:46:29 -0500 Subject: [PATCH 01/12] Span-ified PathMarkupParser v1. --- build/System.Memory.props | 5 + src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + .../Media/PathMarkupParser.cs | 106 +++++++++++++++++- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 build/System.Memory.props diff --git a/build/System.Memory.props b/build/System.Memory.props new file mode 100644 index 0000000000..f3253f8882 --- /dev/null +++ b/build/System.Memory.props @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 0f003a4018..c34752a3ef 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -8,4 +8,5 @@ + \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index e8824753b5..7ef07d7ec8 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -82,7 +82,7 @@ namespace Avalonia.Media /// The path data. public void Parse(string pathData) { - var tokens = ParseTokens(pathData); + var tokens = ParseTokens2(pathData); CreateGeometry(tokens); } @@ -121,6 +121,17 @@ namespace Avalonia.Media return @"(?=[" + stringBuilder + "])"; } + private static IEnumerable ParseTokens2(string s) + { + var commands = new List(); + var span = s.AsSpan(); + while (!span.IsEmpty) + { + commands.Add(CommandToken.Parse(ref span)); + } + return commands; + } + private static IEnumerable ParseTokens(string s) { return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); @@ -494,6 +505,27 @@ namespace Avalonia.Media return new CommandToken(command, isRelative, arguments); } } + + public static CommandToken Parse(ref ReadOnlySpan span) + { + if (!ReadCommand(ref span, out var command, out var isRelative)) + { + throw new InvalidDataException("No path command declared."); + } + + span = span.Slice(1); + span = SkipWhitespace(span); + + var arguments = new List(); + + while (ReadArgument(ref span, out var argument)) + { + arguments.Add(argument.ToString()); + span = ReadSeparator(span); + } + + return new CommandToken(command, isRelative, arguments); + } public FillRule ReadFillRule() { @@ -592,6 +624,28 @@ namespace Avalonia.Media return new Point(origin.X + x, origin.Y + y); } + private static bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + { + span = SkipWhitespace(span); + if (span.IsEmpty) + { + command = default; + relative = false; + return false; + } + + var c = span[0]; + + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) + { + throw new InvalidDataException("Unexpected path command '" + c + "'."); + } + + relative = char.IsLower(c); + + return true; + } + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { ReadWhitespace(reader); @@ -619,6 +673,56 @@ namespace Avalonia.Media return true; } + private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) + { + if (remaining.IsEmpty) + { + argument = ReadOnlySpan.Empty; + return false; + } + + var valid = false; + int i = 0; + if (remaining[i] == '-') + { + i++; + } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + + if (i < remaining.Length && remaining[i] == '.') + { + valid = false; + i++; + } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + + if (!valid) + { + argument = ReadOnlySpan.Empty; + return false; + } + argument = remaining.Slice(0, i); + remaining = remaining.Slice(i); + return true; + } + + private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) + { + span = SkipWhitespace(span); + if (!span.IsEmpty && span[0] == ',') + { + span = span.Slice(1); + } + return SkipWhitespace(span); + } + + private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + { + int i = 0; + for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; + return span.Slice(i); + } + private static void ReadWhitespace(TextReader reader) { int i; From dc9fb565fd35b4feed137f11cacd62b7854c7f47 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 12:50:06 -0500 Subject: [PATCH 02/12] Remove old regex-based parser --- .../Media/PathMarkupParser.cs | 107 +----------------- 1 file changed, 5 insertions(+), 102 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 7ef07d7ec8..c71aed8b66 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -17,7 +17,6 @@ namespace Avalonia.Media /// public class PathMarkupParser : IDisposable { - private static readonly string s_separatorPattern; private static readonly Dictionary s_commands = new Dictionary { @@ -42,7 +41,6 @@ namespace Avalonia.Media static PathMarkupParser() { - s_separatorPattern = CreatesSeparatorPattern(); } /// @@ -82,7 +80,7 @@ namespace Avalonia.Media /// The path data. public void Parse(string pathData) { - var tokens = ParseTokens2(pathData); + var tokens = ParseTokens(pathData); CreateGeometry(tokens); } @@ -107,21 +105,7 @@ namespace Avalonia.Media _isDisposed = true; } - private static string CreatesSeparatorPattern() - { - var stringBuilder = new StringBuilder(); - - foreach (var command in s_commands.Keys) - { - stringBuilder.Append(command); - - stringBuilder.Append(char.ToLower(command)); - } - - return @"(?=[" + stringBuilder + "])"; - } - - private static IEnumerable ParseTokens2(string s) + private static IEnumerable ParseTokens(string s) { var commands = new List(); var span = s.AsSpan(); @@ -132,11 +116,6 @@ namespace Avalonia.Media return commands; } - private static IEnumerable ParseTokens(string s) - { - return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse); - } - private static Point MirrorControlPoint(Point controlPoint, Point center) { var dir = controlPoint - center; @@ -146,7 +125,7 @@ namespace Avalonia.Media private void CreateGeometry(IEnumerable commandTokens) { - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -446,15 +425,13 @@ namespace Avalonia.Media private class CommandToken { - private const string ArgumentExpression = @"-?[0-9]*\.?\d+"; - - private CommandToken(Command command, bool isRelative, IEnumerable arguments) + private CommandToken(Command command, bool isRelative, List arguments) { Command = command; IsRelative = isRelative; - Arguments = new List(arguments); + Arguments = arguments; } public Command Command { get; } @@ -477,34 +454,6 @@ namespace Avalonia.Media private int CurrentPosition { get; set; } private List Arguments { get; } - - public static CommandToken Parse(string s) - { - using (var reader = new StringReader(s)) - { - var command = Command.None; - - var isRelative = false; - - if (!ReadCommand(reader, ref command, ref isRelative)) - { - throw new InvalidDataException("No path command declared."); - } - - var commandArguments = reader.ReadToEnd(); - - var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression); - - var arguments = new List(); - - foreach (Match match in argumentMatches) - { - arguments.Add(match.Value); - } - - return new CommandToken(command, isRelative, arguments); - } - } public static CommandToken Parse(ref ReadOnlySpan span) { @@ -646,33 +595,6 @@ namespace Avalonia.Media return true; } - private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) - { - ReadWhitespace(reader); - - var i = reader.Peek(); - - if (i == -1) - { - return false; - } - - var c = (char)i; - - if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next)) - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } - - command = next; - - relative = char.IsLower(c); - - reader.Read(); - - return true; - } - private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) { if (remaining.IsEmpty) @@ -722,25 +644,6 @@ namespace Avalonia.Media for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; return span.Slice(i); } - - private static void ReadWhitespace(TextReader reader) - { - int i; - - while ((i = reader.Peek()) != -1) - { - var c = (char)i; - - if (char.IsWhiteSpace(c)) - { - reader.Read(); - } - else - { - break; - } - } - } } } } \ No newline at end of file From 6c6f1b48cf62048c9b94c671020558cb1b742ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 15:08:23 -0500 Subject: [PATCH 03/12] Remove unused static constructor. --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index c71aed8b66..e17aafeda3 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -39,10 +39,6 @@ namespace Avalonia.Media private bool? _isOpen; private bool _isDisposed; - static PathMarkupParser() - { - } - /// /// Initializes a new instance of the class. /// From 82802621ead1ff9440b2bb3060697388906b560d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 15:54:48 -0500 Subject: [PATCH 04/12] Update gitignore and add System.Memory package dependency. --- .gitignore | 8 ++++++-- packages.cake | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index a9a8fd36b4..583a2b8a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -176,5 +176,9 @@ nuget Avalonia.XBuild.sln project.lock.json .idea/* -**/obj-Skia/* -**/obj-Direct2D1/* + + +################## +## BenchmarkDotNet +################## +BenchmarkDotNet.Artifacts/ diff --git a/packages.cake b/packages.cake index 7e7e722c82..79147b5f99 100644 --- a/packages.cake +++ b/packages.cake @@ -253,7 +253,7 @@ public class Packages } .Deps(new string[]{null, "netcoreapp2.0"}, "System.ValueTuple", "System.ComponentModel.TypeConverter", "System.ComponentModel.Primitives", - "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter") + "System.Runtime.Serialization.Primitives", "System.Xml.XmlDocument", "System.Xml.ReaderWriter", "System.Memory") .ToArray(), Files = coreLibrariesNuSpecContent .Concat(win32CoreLibrariesNuSpecContent).Concat(net45RuntimePlatform) From 63ec705748fd13ad3fa33aad762eebd3bf315d18 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 16:37:16 -0500 Subject: [PATCH 05/12] Remove extraneous whitespace in tests. --- tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index 35ec38789e..5074d306fd 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Visuals.UnitTests.Media using (var context = new PathGeometryContext(pathGeometry)) using (var parser = new PathMarkupParser(context)) { - parser.Parse("F 1M0,0"); + parser.Parse("F 1M0,0"); Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } From e2f54537982df5136da906478807a71e5e4fe96d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 16:53:00 -0500 Subject: [PATCH 06/12] Remove LINQ usage. --- .../Media/PathMarkupParser.cs | 51 ++++--------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index e17aafeda3..41cd95d319 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,9 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; using Avalonia.Platform; namespace Avalonia.Media @@ -70,17 +67,6 @@ namespace Avalonia.Media Close } - /// - /// Parses the specified path data and writes the result to the geometryContext of this instance. - /// - /// The path data. - public void Parse(string pathData) - { - var tokens = ParseTokens(pathData); - - CreateGeometry(tokens); - } - void IDisposable.Dispose() { Dispose(true); @@ -101,17 +87,6 @@ namespace Avalonia.Media _isDisposed = true; } - private static IEnumerable ParseTokens(string s) - { - var commands = new List(); - var span = s.AsSpan(); - while (!span.IsEmpty) - { - commands.Add(CommandToken.Parse(ref span)); - } - return commands; - } - private static Point MirrorControlPoint(Point controlPoint, Point center) { var dir = controlPoint - center; @@ -119,15 +94,21 @@ namespace Avalonia.Media return center + -dir; } - private void CreateGeometry(IEnumerable commandTokens) + /// + /// Parses the specified path data and writes the result to the geometryContext of this instance. + /// + /// The path data. + public void Parse(string pathData) { + var span = pathData.AsSpan(); _currentPoint = new Point(); - foreach (var commandToken in commandTokens) + while(!span.IsEmpty) { + var commandToken = CommandToken.Parse(ref span); try { - while (true) + do { switch (commandToken.Command) { @@ -169,14 +150,7 @@ namespace Avalonia.Media default: throw new NotSupportedException("Unsupported command"); } - - if (commandToken.HasImplicitCommands) - { - continue; - } - - break; - } + } while (commandToken.HasImplicitCommands); } catch (InvalidDataException) { @@ -235,11 +209,6 @@ namespace Avalonia.Media CreateFigure(); - if (!commandToken.HasImplicitCommands) - { - return; - } - while (commandToken.HasImplicitCommands) { AddLine(commandToken); From c31edafb553c70ea64087248fa84addac77fb1b8 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 17:05:33 -0500 Subject: [PATCH 07/12] Clear up code logic. --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 41cd95d319..b0c7ed7e95 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -213,14 +213,11 @@ namespace Avalonia.Media { AddLine(commandToken); - if (commandToken.IsRelative) + if (!commandToken.IsRelative) { - continue; + _currentPoint = currentPoint; + CreateFigure(); } - - _currentPoint = currentPoint; - - CreateFigure(); } } From 11d3e7f7e3cd26f8c92fb09bc4449b40dcac935f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 17:10:56 -0500 Subject: [PATCH 08/12] Make _isOpen non-nullable since the only two values that were used were true and null. --- .../Media/PathMarkupParser.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index b0c7ed7e95..8f1d8ebc71 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -33,7 +33,7 @@ namespace Avalonia.Media private IGeometryContext _geometryContext; private Point _currentPoint; private Point? _previousControlPoint; - private bool? _isOpen; + private bool _isOpen; private bool _isDisposed; /// @@ -162,7 +162,7 @@ namespace Avalonia.Media } } - if (_isOpen != null) + if (_isOpen) { _geometryContext.EndFigure(false); } @@ -170,7 +170,7 @@ namespace Avalonia.Media private void CreateFigure() { - if (_isOpen != null) + if (_isOpen) { _geometryContext.EndFigure(false); } @@ -189,14 +189,14 @@ namespace Avalonia.Media private void CloseFigure() { - if (_isOpen == true) + if (_isOpen) { _geometryContext.EndFigure(true); } _previousControlPoint = null; - _isOpen = null; + _isOpen = false; } private void AddMove(CommandToken commandToken) @@ -227,7 +227,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -241,7 +241,7 @@ namespace Avalonia.Media ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) : _currentPoint.WithX(commandToken.ReadDouble()); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -255,7 +255,7 @@ namespace Avalonia.Media ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) : _currentPoint.WithY(commandToken.ReadDouble()); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -279,7 +279,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -301,7 +301,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -326,7 +326,7 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -349,7 +349,7 @@ namespace Avalonia.Media _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } @@ -373,7 +373,7 @@ namespace Avalonia.Media ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - if (_isOpen == null) + if (!_isOpen) { CreateFigure(); } From 85159c069c08df23983fa0163c78bf851f46a515 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 20 Jun 2018 18:52:04 -0500 Subject: [PATCH 09/12] Span-ification v2. Remove CommandToken and just operate directly on the single span. Memory allocation is now down to less than 5KB. --- .../Media/PathMarkupParser.cs | 534 ++++++++---------- 1 file changed, 238 insertions(+), 296 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 8f1d8ebc71..384c96a7d6 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -105,61 +105,64 @@ namespace Avalonia.Media while(!span.IsEmpty) { - var commandToken = CommandToken.Parse(ref span); - try + if(!ReadCommand(ref span, out var command, out var relative)) { - do - { - switch (commandToken.Command) - { - case Command.None: - break; - case Command.FillRule: - SetFillRule(commandToken); - break; - case Command.Move: - AddMove(commandToken); - break; - case Command.Line: - AddLine(commandToken); - break; - case Command.HorizontalLine: - AddHorizontalLine(commandToken); - break; - case Command.VerticalLine: - AddVerticalLine(commandToken); - break; - case Command.CubicBezierCurve: - AddCubicBezierCurve(commandToken); - break; - case Command.QuadraticBezierCurve: - AddQuadraticBezierCurve(commandToken); - break; - case Command.SmoothCubicBezierCurve: - AddSmoothCubicBezierCurve(commandToken); - break; - case Command.SmoothQuadraticBezierCurve: - AddSmoothQuadraticBezierCurve(commandToken); - break; - case Command.Arc: - AddArc(commandToken); - break; - case Command.Close: - CloseFigure(); - break; - default: - throw new NotSupportedException("Unsupported command"); - } - } while (commandToken.HasImplicitCommands); - } - catch (InvalidDataException) - { - break; + return; } - catch (NotSupportedException) + + bool initialCommand = true; + + do { - break; - } + if (!initialCommand) + { + span = ReadSeparator(span); + } + + switch (command) + { + case Command.None: + break; + case Command.FillRule: + SetFillRule(ref span); + break; + case Command.Move: + AddMove(ref span, relative); + break; + case Command.Line: + AddLine(ref span, relative); + break; + case Command.HorizontalLine: + AddHorizontalLine(ref span, relative); + break; + case Command.VerticalLine: + AddVerticalLine(ref span, relative); + break; + case Command.CubicBezierCurve: + AddCubicBezierCurve(ref span, relative); + break; + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(ref span, relative); + break; + case Command.SmoothCubicBezierCurve: + AddSmoothCubicBezierCurve(ref span, relative); + break; + case Command.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(ref span, relative); + break; + case Command.Arc: + AddArc(ref span, relative); + break; + case Command.Close: + CloseFigure(); + break; + default: + throw new NotSupportedException("Unsupported command"); + } + + initialCommand = false; + } while (PeekArgument(span)); + } if (_isOpen) @@ -180,11 +183,28 @@ namespace Avalonia.Media _isOpen = true; } - private void SetFillRule(CommandToken commandToken) + private void SetFillRule(ref ReadOnlySpan span) { - var fillRule = commandToken.ReadFillRule(); + if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1) + { + throw new InvalidDataException("Invalid fill rule."); + } + + FillRule rule; - _geometryContext.SetFillRule(fillRule); + switch (fillRule[0]) + { + case '0': + rule = FillRule.EvenOdd; + break; + case '1': + rule = FillRule.NonZero; + break; + default: + throw new InvalidDataException("Invalid fill rule"); + } + + _geometryContext.SetFillRule(rule); } private void CloseFigure() @@ -199,21 +219,22 @@ namespace Avalonia.Media _isOpen = false; } - private void AddMove(CommandToken commandToken) + private void AddMove(ref ReadOnlySpan span, bool relative) { - var currentPoint = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var currentPoint = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); _currentPoint = currentPoint; CreateFigure(); - while (commandToken.HasImplicitCommands) + while (PeekArgument(span)) { - AddLine(commandToken); + span = ReadSeparator(span); + AddLine(ref span, relative); - if (!commandToken.IsRelative) + if (!relative) { _currentPoint = currentPoint; CreateFigure(); @@ -221,11 +242,11 @@ namespace Avalonia.Media } } - private void AddLine(CommandToken commandToken) + private void AddLine(ref ReadOnlySpan span, bool relative) { - _currentPoint = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + _currentPoint = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -235,11 +256,11 @@ namespace Avalonia.Media _geometryContext.LineTo(_currentPoint); } - private void AddHorizontalLine(CommandToken commandToken) + private void AddHorizontalLine(ref ReadOnlySpan span, bool relative) { - _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) - : _currentPoint.WithX(commandToken.ReadDouble()); + _currentPoint = relative + ? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y) + : _currentPoint.WithX(ReadDouble(ref span)); if (!_isOpen) { @@ -249,11 +270,11 @@ namespace Avalonia.Media _geometryContext.LineTo(_currentPoint); } - private void AddVerticalLine(CommandToken commandToken) + private void AddVerticalLine(ref ReadOnlySpan span, bool relative) { - _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) - : _currentPoint.WithY(commandToken.ReadDouble()); + _currentPoint = relative + ? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span)) + : _currentPoint.WithY(ReadDouble(ref span)); if (!_isOpen) { @@ -263,21 +284,25 @@ namespace Avalonia.Media _geometryContext.LineTo(_currentPoint); } - private void AddCubicBezierCurve(CommandToken commandToken) + private void AddCubicBezierCurve(ref ReadOnlySpan span, bool relative) { - var point1 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var point1 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); - var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + span = ReadSeparator(span); + + var point2 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); _previousControlPoint = point2; - var point3 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + span = ReadSeparator(span); + + var point3 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -289,17 +314,19 @@ namespace Avalonia.Media _currentPoint = point3; } - private void AddQuadraticBezierCurve(CommandToken commandToken) + private void AddQuadraticBezierCurve(ref ReadOnlySpan span, bool relative) { - var start = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var start = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); _previousControlPoint = start; - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + span = ReadSeparator(span); + + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -311,15 +338,17 @@ namespace Avalonia.Media _currentPoint = end; } - private void AddSmoothCubicBezierCurve(CommandToken commandToken) + private void AddSmoothCubicBezierCurve(ref ReadOnlySpan span, bool relative) { - var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var point2 = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); + + span = ReadSeparator(span); - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (_previousControlPoint != null) { @@ -338,11 +367,11 @@ namespace Avalonia.Media _currentPoint = end; } - private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) + private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan span, bool relative) { - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (_previousControlPoint != null) { @@ -359,19 +388,25 @@ namespace Avalonia.Media _currentPoint = end; } - private void AddArc(CommandToken commandToken) + private void AddArc(ref ReadOnlySpan span, bool relative) { - var size = commandToken.ReadSize(); + var size = ReadSize(ref span); - var rotationAngle = commandToken.ReadDouble(); + span = ReadSeparator(span); - var isLargeArc = commandToken.ReadBool(); + var rotationAngle = ReadDouble(ref span); + span = ReadSeparator(span); + var isLargeArc = ReadBool(ref span); - var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + span = ReadSeparator(span); + + var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; + + span = ReadSeparator(span); - var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + var end = relative + ? ReadRelativePoint(ref span, _currentPoint) + : ReadPoint(ref span); if (!_isOpen) { @@ -385,227 +420,134 @@ namespace Avalonia.Media _previousControlPoint = null; } - private class CommandToken + private static bool PeekArgument(ReadOnlySpan span) { - private CommandToken(Command command, bool isRelative, List arguments) - { - Command = command; - - IsRelative = isRelative; - - Arguments = arguments; - } - - public Command Command { get; } + span = SkipWhitespace(span); - public bool IsRelative { get; } + return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || char.IsDigit(span[0])); + } - public bool HasImplicitCommands + private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) + { + remaining = SkipWhitespace(remaining); + if (remaining.IsEmpty) { - get - { - if (CurrentPosition == 0 && Arguments.Count > 0) - { - return true; - } - - return CurrentPosition < Arguments.Count - 1; - } + argument = ReadOnlySpan.Empty; + return false; } - private int CurrentPosition { get; set; } - - private List Arguments { get; } - - public static CommandToken Parse(ref ReadOnlySpan span) + var valid = false; + int i = 0; + if (remaining[i] == '-') { - if (!ReadCommand(ref span, out var command, out var isRelative)) - { - throw new InvalidDataException("No path command declared."); - } - - span = span.Slice(1); - span = SkipWhitespace(span); - - var arguments = new List(); - - while (ReadArgument(ref span, out var argument)) - { - arguments.Add(argument.ToString()); - span = ReadSeparator(span); - } - - return new CommandToken(command, isRelative, arguments); + i++; } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; - public FillRule ReadFillRule() + if (i < remaining.Length && remaining[i] == '.') { - if (CurrentPosition == Arguments.Count) - { - throw new InvalidDataException("Invalid fill rule"); - } - - var value = Arguments[CurrentPosition]; - - CurrentPosition++; - - switch (value) - { - case "0": - { - return FillRule.EvenOdd; - } - - case "1": - { - return FillRule.NonZero; - } - - default: - throw new InvalidDataException("Invalid fill rule"); - } + valid = false; + i++; } + for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true; - public bool ReadBool() + if (!valid) { - if (CurrentPosition == Arguments.Count) - { - throw new InvalidDataException("Invalid boolean value"); - } - - var value = Arguments[CurrentPosition]; - - CurrentPosition++; - - switch (value) - { - case "1": - { - return true; - } - - case "0": - { - return false; - } - - default: - throw new InvalidDataException("Invalid boolean value"); - } + argument = ReadOnlySpan.Empty; + return false; } + argument = remaining.Slice(0, i); + remaining = remaining.Slice(i); + return true; + } - public double ReadDouble() - { - if (CurrentPosition == Arguments.Count) - { - throw new InvalidDataException("Invalid double value"); - } - - var value = Arguments[CurrentPosition]; - - CurrentPosition++; - - return double.Parse(value, CultureInfo.InvariantCulture); - } - public Size ReadSize() + private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) + { + span = SkipWhitespace(span); + if (!span.IsEmpty && span[0] == ',') { - var width = ReadDouble(); - - var height = ReadDouble(); - - return new Size(width, height); + span = span.Slice(1); } + return span; + } - public Point ReadPoint() - { - var x = ReadDouble(); - - var y = ReadDouble(); - - return new Point(x, y); - } + private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + { + int i = 0; + for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; + return span.Slice(i); + } - public Point ReadRelativePoint(Point origin) + private bool ReadBool(ref ReadOnlySpan span) + { + if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1) { - var x = ReadDouble(); - - var y = ReadDouble(); - - return new Point(origin.X + x, origin.Y + y); + throw new InvalidDataException("Invalid bool rule."); } - - private static bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + + switch (boolValue[0]) { - span = SkipWhitespace(span); - if (span.IsEmpty) - { - command = default; - relative = false; + case '0': return false; - } - - var c = span[0]; - - if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) - { - throw new InvalidDataException("Unexpected path command '" + c + "'."); - } - - relative = char.IsLower(c); - - return true; + case '1': + return true; + default: + throw new InvalidDataException("Invalid bool rule"); } + } - private static bool ReadArgument(ref ReadOnlySpan remaining, out ReadOnlySpan argument) + private double ReadDouble(ref ReadOnlySpan span) + { + if (!ReadArgument(ref span, out var doubleValue)) { - if (remaining.IsEmpty) - { - argument = ReadOnlySpan.Empty; - return false; - } + throw new InvalidDataException("Invalid double value"); + } - var valid = false; - int i = 0; - if (remaining[i] == '-') - { - i++; - } - for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture); + } - if (i < remaining.Length && remaining[i] == '.') - { - valid = false; - i++; - } - for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true ; + private Size ReadSize(ref ReadOnlySpan span) + { + var width = ReadDouble(ref span); + span = ReadSeparator(span); + var height = ReadDouble(ref span); + return new Size(width, height); + } - if (!valid) - { - argument = ReadOnlySpan.Empty; - return false; - } - argument = remaining.Slice(0, i); - remaining = remaining.Slice(i); - return true; - } + private Point ReadPoint(ref ReadOnlySpan span) + { + var x = ReadDouble(ref span); + span = ReadSeparator(span); + var y = ReadDouble(ref span); + return new Point(x, y); + } - private static ReadOnlySpan ReadSeparator(ReadOnlySpan span) + private Point ReadRelativePoint(ref ReadOnlySpan span, Point origin) + { + var x = ReadDouble(ref span); + span = ReadSeparator(span); + var y = ReadDouble(ref span); + return new Point(origin.X + x, origin.Y + y); + } + + private bool ReadCommand(ref ReadOnlySpan span, out Command command, out bool relative) + { + span = SkipWhitespace(span); + if (span.IsEmpty) { - span = SkipWhitespace(span); - if (!span.IsEmpty && span[0] == ',') - { - span = span.Slice(1); - } - return SkipWhitespace(span); + command = default; + relative = false; + return false; } - - private static ReadOnlySpan SkipWhitespace(ReadOnlySpan span) + var c = span[0]; + if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command)) { - int i = 0; - for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ; - return span.Slice(i); + throw new InvalidDataException("Unexpected path command '" + c + "'."); } + relative = char.IsLower(c); + span = span.Slice(1); + return true; } } } \ No newline at end of file From af7e139ed4cb7e06850ccef07d6cc8e934ca061d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Jun 2018 00:33:24 +0200 Subject: [PATCH 10/12] Added failing test for #1698. --- .../SelectorTests_Class.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs index b41c21fbf4..75599925b7 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Class.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Reactive.Linq; using System.Threading.Tasks; @@ -8,6 +9,7 @@ using Moq; using Avalonia.Controls; using Avalonia.Styling; using Xunit; +using System.Collections.Generic; namespace Avalonia.Styling.UnitTests { @@ -117,6 +119,28 @@ namespace Avalonia.Styling.UnitTests Assert.False(await activator.Take(1)); } + [Fact] + public void Only_Notifies_When_Result_Changes() + { + // Test for #1698 + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).Class("foo"); + var activator = target.Match(control).ObservableResult; + var result = new List(); + + using (activator.Subscribe(x => result.Add(x))) + { + control.Classes.Add("bar"); + control.Classes.Remove("foo"); + } + + Assert.Equal(new[] { true, false }, result); + } + public class Control1 : TestControlBase { } From 114b393813d38683ebc4ea013555ed5f7fa6956b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Jun 2018 00:33:55 +0200 Subject: [PATCH 11/12] Only publish distinct values from ClassObserver. Fixes #1698. --- .../Styling/TypeNameAndClassSelector.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs index 93d4e14f27..94c0b75c6e 100644 --- a/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs @@ -202,6 +202,7 @@ namespace Avalonia.Styling { readonly IList _match; IAvaloniaReadOnlyList _classes; + bool _value; public ClassObserver(IAvaloniaReadOnlyList classes, IList match) { @@ -210,18 +211,29 @@ namespace Avalonia.Styling } protected override void Deinitialize() => _classes.CollectionChanged -= ClassesChanged; - protected override void Initialize() => _classes.CollectionChanged += ClassesChanged; + + protected override void Initialize() + { + _value = GetResult(); + _classes.CollectionChanged += ClassesChanged; + } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(GetResult()); + observer.OnNext(_value); } private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action != NotifyCollectionChangedAction.Move) { - PublishNext(GetResult()); + var value = GetResult(); + + if (value != _value) + { + PublishNext(GetResult()); + _value = value; + } } } From 617dd6a5ab9718043440e4cccbece7ae8e566a01 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 26 Jun 2018 15:43:33 +0100 Subject: [PATCH 12/12] fix win32 clipboard settext async. --- src/Windows/Avalonia.Win32/ClipboardImpl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 3dae8e37d9..a908c9e1e2 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -57,6 +57,9 @@ namespace Avalonia.Win32 } await OpenClipboard(); + + UnmanagedMethods.EmptyClipboard(); + try { var hGlobal = Marshal.StringToHGlobalUni(text);