Browse Source

Merge branch 'feature/ApplicationExitMode' of https://github.com/Gillibald/Avalonia into feature/ApplicationExitMode

Benedikt Schroeder 7 years ago
parent
commit
178ca30fdd

+ 20 - 1
src/Avalonia.Visuals/Media/PathGeometry.cs

@@ -5,6 +5,7 @@ using System;
 using Avalonia.Collections;
 using Avalonia.Metadata;
 using Avalonia.Platform;
+using Avalonia.Visuals.Platform;
 
 namespace Avalonia.Media
 {
@@ -28,7 +29,7 @@ namespace Avalonia.Media
 
         static PathGeometry()
         {
-            FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) => 
+            FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) =>
                 s.OnFiguresChanged(e.NewValue as PathFigures));
         }
 
@@ -40,6 +41,24 @@ namespace Avalonia.Media
             Figures = new PathFigures();
         }
 
+        /// <summary>
+        /// Parses the specified path data to a <see cref="PathGeometry"/>.
+        /// </summary>
+        /// <param name="pathData">The s.</param>
+        /// <returns></returns>
+        public static new PathGeometry Parse(string pathData)
+        {
+            var pathGeometry = new PathGeometry();
+
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse(pathData);
+            }
+
+            return pathGeometry;
+        }
+
         /// <summary>
         /// Gets or sets the figures.
         /// </summary>

+ 505 - 284
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@@ -5,50 +5,61 @@ 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
 {
     /// <summary>
     /// Parses a path markup string.
     /// </summary>
-    public class PathMarkupParser
+    public class PathMarkupParser : IDisposable
     {
-        private static readonly Dictionary<char, Command> Commands = new Dictionary<char, Command>
-        {
-            { 'F', Command.FillRule },
-            { 'M', Command.Move },
-            { '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 },
-        };
-
-        private static readonly Dictionary<char, FillRule> FillRules = new Dictionary<char, FillRule>
+        private static readonly string s_separatorPattern;
+        private static readonly Dictionary<char, Command> s_commands =
+            new Dictionary<char, Command>
+                {
+                    { 'F', Command.FillRule },
+                    { 'M', Command.Move },
+                    { '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 },
+                };
+
+        private IGeometryContext _geometryContext;
+        private Point _currentPoint;
+        private Point? _previousControlPoint;
+        private bool? _isOpen;
+        private bool _isDisposed;
+
+        static PathMarkupParser()
         {
-            {'0', FillRule.EvenOdd },
-            {'1', FillRule.NonZero }
-        };
-
-        private readonly StreamGeometryContext _context;
+            s_separatorPattern = CreatesSeparatorPattern();
+        }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
         /// </summary>
-        /// <param name="context">The context for the geometry.</param>
-        public PathMarkupParser(StreamGeometryContext context)
+        /// <param name="geometryContext">The geometry context.</param>
+        /// <exception cref="ArgumentNullException">geometryContext</exception>
+        public PathMarkupParser(IGeometryContext geometryContext)
         {
-            _context = context;
+            if (geometryContext == null)
+            {
+                throw new ArgumentNullException(nameof(geometryContext));
+            }
+
+            _geometryContext = geometryContext;
         }
 
-        /// <summary>
-        /// Defines the command currently being processed.
-        /// </summary>
         private enum Command
         {
             None,
@@ -62,358 +73,568 @@ namespace Avalonia.Media
             SmoothCubicBezierCurve,
             SmoothQuadraticBezierCurve,
             Arc,
-            Close,
+            Close
         }
 
         /// <summary>
-        /// Parses the specified markup string.
+        /// Parses the specified path data and writes the result to the geometryContext of this instance.
         /// </summary>
-        /// <param name="s">The markup string.</param>
-        public void Parse(string s)
+        /// <param name="pathData">The path data.</param>
+        public void Parse(string pathData)
+        {
+            var tokens = ParseTokens(pathData);
+
+            CreateGeometry(tokens);
+        }
+
+        void IDisposable.Dispose()
+        {
+            Dispose(true);
+        }
+
+        protected virtual void Dispose(bool disposing)
         {
-            bool openFigure = false;
+            if (_isDisposed)
+            {
+                return;
+            }
 
-            using (StringReader reader = new StringReader(s))
+            if (disposing)
             {
-                Command command = Command.None;
-                Point point = new Point();
-                bool relative = false;        
-                Point? previousControlPoint = null;
+                _geometryContext = null;
+            }
+
+            _isDisposed = true;
+        }
+
+        private static string CreatesSeparatorPattern()
+        {
+            var stringBuilder = new StringBuilder();
 
-                while (ReadCommand(reader, ref command, ref relative))
+            foreach (var command in s_commands.Keys)
+            {
+                stringBuilder.Append(command);
+
+                stringBuilder.Append(char.ToLower(command));
+            }
+
+            return @"(?=[" + stringBuilder + "])";
+        }
+
+        private static IEnumerable<CommandToken> 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;
+
+            return center + -dir;
+        }
+
+        private void CreateGeometry(IEnumerable<CommandToken> commandTokens)
+        {
+            _currentPoint = new Point();           
+
+            foreach (var commandToken in commandTokens)
+            {
+                try
                 {
-                    switch (command)
+                    while (true)
                     {
-                        case Command.FillRule:
-                            _context.SetFillRule(ReadFillRule(reader));
-                            previousControlPoint = null;
-                            break;
-
-                        case Command.Move:
-                            if (openFigure)
-                            {
-                                _context.EndFigure(false);
-                            }
-
-                            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:
-                            if (!relative)
-                            {
-                                point = point.WithX(ReadDouble(reader));
-                            }
-                            else
-                            {
-                                point = new Point(point.X + ReadDouble(reader), point.Y);
-                            }
-
-                            _context.LineTo(point);
-                            previousControlPoint = null;
-                            break;
-
-                        case Command.VerticalLine:
-                            if (!relative)
-                            {
-                                point = point.WithY(ReadDouble(reader));
-                            }
-                            else
-                            {
-                                point = new Point(point.X, point.Y + ReadDouble(reader));
-                            }
-
-                            _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);
+                        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.SmoothQuadraticBezierCurve:
-                            {
-                                Point end = ReadPoint(reader, point, relative);
-                                
-                                if(previousControlPoint != null)
-                                    previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point);
-                                
-                                _context.QuadraticBezierTo(previousControlPoint ?? point, end);
-                                point = end;
+                            case Command.CubicBezierCurve:
+                                AddCubicBezierCurve(commandToken);
                                 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);
+                            case Command.QuadraticBezierCurve:
+                                AddQuadraticBezierCurve(commandToken);
                                 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;
+                            case Command.SmoothCubicBezierCurve:
+                                AddSmoothCubicBezierCurve(commandToken);
                                 break;
-                            }
-
-                        case Command.Arc:
-                            {
-                                Size size = ReadSize(reader);
-                                ReadSeparator(reader);
-                                double rotationAngle = ReadDouble(reader);
-                                ReadSeparator(reader);
-                                bool isLargeArc = ReadBool(reader);
-                                ReadSeparator(reader);
-                                SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
-                                ReadSeparator(reader);
-                                point = ReadPoint(reader, point, relative);
-
-                                _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
-                                previousControlPoint = null;
+                            case Command.SmoothQuadraticBezierCurve:
+                                AddSmoothQuadraticBezierCurve(commandToken);
                                 break;
-                            }
+                            case Command.Arc:
+                                AddArc(commandToken);
+                                break;
+                            case Command.Close:
+                                CloseFigure();
+                                break;
+                            default:
+                                throw new NotSupportedException("Unsupported command");
+                        }
 
-                        case Command.Close:
-                            _context.EndFigure(true);
-                            openFigure = false;
-                            previousControlPoint = null;
-                            break;
+                        if (commandToken.HasImplicitCommands)
+                        {
+                            continue;
+                        }
 
-                        default:
-                            throw new NotSupportedException("Unsupported command");
+                        break;
                     }
                 }
-
-                if (openFigure)
+                catch (InvalidDataException)
+                {
+                    break;
+                }
+                catch (NotSupportedException)
                 {
-                    _context.EndFigure(false);
+                    break;
                 }
             }
+
+            if (_isOpen != null)
+            {
+                _geometryContext.EndFigure(false);
+            }
         }
 
-        private Point MirrorControlPoint(Point controlPoint, Point center)
+        private void CreateFigure()
         {
-            Point dir = (controlPoint - center);
-            return center + -dir;
+            if (_isOpen != null)
+            {
+                _geometryContext.EndFigure(false);
+            }
+
+            _geometryContext.BeginFigure(_currentPoint);
+
+            _isOpen = true;
         }
 
-        private static bool ReadCommand(
-            StringReader reader,
-            ref Command command,
-            ref bool relative)
+        private void SetFillRule(CommandToken commandToken)
         {
-            ReadWhitespace(reader);
+            var fillRule = commandToken.ReadFillRule();
 
-            int i = reader.Peek();
+            _geometryContext.SetFillRule(fillRule);
+        }
 
-            if (i == -1)
+        private void CloseFigure()
+        {
+            if (_isOpen == true)
             {
-                return false;
+                _geometryContext.EndFigure(true);
             }
-            else
+
+            _previousControlPoint = null;
+
+            _isOpen = null;
+        }
+
+        private void AddMove(CommandToken commandToken)
+        {
+            var currentPoint = commandToken.IsRelative
+                                   ? commandToken.ReadRelativePoint(_currentPoint)
+                                   : commandToken.ReadPoint();
+
+            _currentPoint = currentPoint;
+
+            CreateFigure();
+
+            if (!commandToken.HasImplicitCommands)
+            {
+                return;
+            }
+
+            while (commandToken.HasImplicitCommands)
             {
-                char c = (char)i;
-                Command next = Command.None;
+                AddLine(commandToken);
 
-                if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next))
+                if (commandToken.IsRelative)
                 {
-                    if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') &&
-                        (command != Command.None))
-                    {
-                        return true;
-                    }
-                    else
-                    {
-                        throw new InvalidDataException("Unexpected path command '" + c + "'.");
-                    }
+                    continue;
                 }
 
-                command = next;
-                relative = char.IsLower(c);
-                reader.Read();
-                return true;
+                _currentPoint = currentPoint;
+
+                CreateFigure();
             }
         }
 
-        private static FillRule ReadFillRule(StringReader reader)
+        private void AddLine(CommandToken commandToken)
         {
-            int i = reader.Read();
-            if (i == -1)
+            _currentPoint = commandToken.IsRelative
+                                ? commandToken.ReadRelativePoint(_currentPoint)
+                                : commandToken.ReadPoint();
+
+            if (_isOpen == null)
             {
-                throw new InvalidDataException("Invalid fill rule");
+                CreateFigure();
             }
-            char c = (char)i;
-            FillRule rule;
 
-            if (!FillRules.TryGetValue(c, out rule))
+            _geometryContext.LineTo(_currentPoint);
+        }
+
+        private void AddHorizontalLine(CommandToken commandToken)
+        {
+            _currentPoint = commandToken.IsRelative
+                                ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
+                                : _currentPoint.WithX(commandToken.ReadDouble());
+
+            if (_isOpen == null)
             {
-                throw new InvalidDataException("Invalid fill rule");
+                CreateFigure();
             }
 
-            return rule;
+            _geometryContext.LineTo(_currentPoint);
         }
 
-        private static double ReadDouble(StringReader reader)
+        private void AddVerticalLine(CommandToken commandToken)
         {
-            ReadWhitespace(reader);
+            _currentPoint = commandToken.IsRelative
+                                ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
+                                : _currentPoint.WithY(commandToken.ReadDouble());
 
-            // TODO: Handle Infinity, NaN and scientific notation.
-            StringBuilder b = new StringBuilder();
-            bool readSign = false;
-            bool readPoint = false;
-            bool readExponent = false;
-            int i;
-
-            while ((i = reader.Peek()) != -1)
+            if (_isOpen == null)
             {
-                char c = char.ToUpperInvariant((char)i);
+                CreateFigure();
+            }
 
-                if (((c == '+' || c == '-') && !readSign) ||
-                    (c == '.' && !readPoint) ||
-                    (c == 'E' && !readExponent) ||
-                    char.IsDigit(c))
-                {
-                    if (b.Length != 0 && !readExponent && c == '-')
-                        break;
-                    
-                    b.Append(c);
-                    reader.Read();
+            _geometryContext.LineTo(_currentPoint);
+        }
 
-                    if (!readSign)
-                    {
-                        readSign = c == '+' || c == '-';
-                    }
+        private void AddCubicBezierCurve(CommandToken commandToken)
+        {
+            var point1 = commandToken.IsRelative
+                             ? commandToken.ReadRelativePoint(_currentPoint)
+                             : commandToken.ReadPoint();
 
-                    if (!readPoint)
-                    {
-                        readPoint = c == '.';
-                    }
+            var point2 = commandToken.IsRelative
+                             ? commandToken.ReadRelativePoint(_currentPoint)
+                             : commandToken.ReadPoint();
 
-                    if (c == 'E')
-                    {
-                        readSign = false;
-                        readExponent = true;
-                    }
-                }
-                else
-                {
-                    break;
-                }
+            _previousControlPoint = point2;
+
+            var point3 = commandToken.IsRelative
+                             ? commandToken.ReadRelativePoint(_currentPoint)
+                             : commandToken.ReadPoint();
+
+            if (_isOpen == null)
+            {
+                CreateFigure();
             }
 
-            return double.Parse(b.ToString(), CultureInfo.InvariantCulture);
+            _geometryContext.CubicBezierTo(point1, point2, point3);
+
+            _currentPoint = point3;
         }
 
-        private static Point ReadPoint(StringReader reader, Point current, bool relative)
+        private void AddQuadraticBezierCurve(CommandToken commandToken)
         {
-            if (!relative)
+            var start = commandToken.IsRelative
+                            ? commandToken.ReadRelativePoint(_currentPoint)
+                            : commandToken.ReadPoint();
+
+            _previousControlPoint = start;
+
+            var end = commandToken.IsRelative
+                          ? commandToken.ReadRelativePoint(_currentPoint)
+                          : commandToken.ReadPoint();
+
+            if (_isOpen == null)
             {
-                current = new Point();
+                CreateFigure();
             }
 
-            ReadWhitespace(reader);
-            double x = current.X + ReadDouble(reader);
-            ReadSeparator(reader);
-            double y = current.Y + ReadDouble(reader);
-            return new Point(x, y);
+            _geometryContext.QuadraticBezierTo(start, end);
+
+            _currentPoint = end;
         }
 
-        private static Size ReadSize(StringReader reader)
+        private void AddSmoothCubicBezierCurve(CommandToken commandToken)
         {
-            ReadWhitespace(reader);
-            double x = ReadDouble(reader);
-            ReadSeparator(reader);
-            double y = ReadDouble(reader);
-            return new Size(x, y);
+            var point2 = commandToken.IsRelative
+                             ? commandToken.ReadRelativePoint(_currentPoint)
+                             : commandToken.ReadPoint();
+
+            var end = commandToken.IsRelative
+                          ? commandToken.ReadRelativePoint(_currentPoint)
+                          : commandToken.ReadPoint();
+
+            if (_previousControlPoint != null)
+            {
+                _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
+            }
+
+            if (_isOpen == null)
+            {
+                CreateFigure();
+            }
+
+            _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end);
+
+            _previousControlPoint = point2;
+
+            _currentPoint = end;
         }
 
-        private static bool ReadBool(StringReader reader)
+        private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
         {
-            return ReadDouble(reader) != 0;
+            var end = commandToken.IsRelative
+                          ? commandToken.ReadRelativePoint(_currentPoint)
+                          : commandToken.ReadPoint();
+
+            if (_previousControlPoint != null)
+            {
+                _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
+            }
+
+            if (_isOpen == null)
+            {
+                CreateFigure();
+            }
+
+            _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end);
+
+            _currentPoint = end;
         }
 
-        private static Point ReadRelativePoint(StringReader reader, Point lastPoint)
+        private void AddArc(CommandToken commandToken)
         {
-            ReadWhitespace(reader);
-            double x = ReadDouble(reader);
-            ReadSeparator(reader);
-            double y = ReadDouble(reader);
-            return new Point(lastPoint.X + x, lastPoint.Y + y);
+            var size = commandToken.ReadSize();
+
+            var rotationAngle = commandToken.ReadDouble();
+
+            var isLargeArc = commandToken.ReadBool();
+
+            var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
+
+            var end = commandToken.IsRelative
+                          ? commandToken.ReadRelativePoint(_currentPoint)
+                          : commandToken.ReadPoint();
+
+            if (_isOpen == null)
+            {
+                CreateFigure();
+            }
+
+            _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection);
+
+            _currentPoint = end;
+
+            _previousControlPoint = null;
         }
 
-        private static void ReadSeparator(StringReader reader)
+        private class CommandToken
         {
-            int i;
-            bool readComma = false;
+            private const string ArgumentExpression = @"-?[0-9]*\.?\d+";
 
-            while ((i = reader.Peek()) != -1)
+            private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments)
             {
-                char c = (char)i;
+                Command = command;
+
+                IsRelative = isRelative;
+
+                Arguments = new List<string>(arguments);
+            }
+
+            public Command Command { get; }
 
-                if (char.IsWhiteSpace(c))
+            public bool IsRelative { get; }
+
+            public bool HasImplicitCommands
+            {
+                get
                 {
-                    reader.Read();
+                    if (CurrentPosition == 0 && Arguments.Count > 0)
+                    {
+                        return true;
+                    }
+
+                    return CurrentPosition < Arguments.Count - 1;
                 }
-                else if (c == ',')
+            }
+
+            private int CurrentPosition { get; set; }
+
+            private List<string> Arguments { get; }
+
+            public static CommandToken Parse(string s)
+            {
+                using (var reader = new StringReader(s))
                 {
-                    if (readComma)
+                    var command = Command.None;
+
+                    var isRelative = false;
+
+                    if (!ReadCommand(reader, ref command, ref isRelative))
                     {
-                        throw new InvalidDataException("Unexpected ','.");
+                        throw new InvalidDataException("No path command declared.");
                     }
 
-                    readComma = true;
-                    reader.Read();
+                    var commandArguments = reader.ReadToEnd();
+
+                    var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression);
+
+                    var arguments = new List<string>();
+
+                    foreach (Match match in argumentMatches)
+                    {
+                        arguments.Add(match.Value);
+                    }
+
+                    return new CommandToken(command, isRelative, arguments);
+                }
+            }
+
+            public FillRule ReadFillRule()
+            {
+                if (CurrentPosition == Arguments.Count)
+                {
+                    throw new InvalidDataException("Invalid fill rule");
                 }
-                else
+
+                var value = Arguments[CurrentPosition];
+
+                CurrentPosition++;
+
+                switch (value)
                 {
-                    break;
+                    case "0":
+                        {
+                            return FillRule.EvenOdd;
+                        }
+
+                    case "1":
+                        {
+                            return FillRule.NonZero;
+                        }
+
+                    default:
+                        throw new InvalidDataException("Invalid fill rule");
                 }
             }
-        }
 
-        private static void ReadWhitespace(StringReader reader)
-        {
-            int i;
+            public bool ReadBool()
+            {
+                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");
+                }
+            }
+
+            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()
+            {
+                var width = ReadDouble();
+
+                var height = ReadDouble();
+
+                return new Size(width, height);
+            }
+
+            public Point ReadPoint()
+            {
+                var x = ReadDouble();
+
+                var y = ReadDouble();
+
+                return new Point(x, y);
+            }
+
+            public Point ReadRelativePoint(Point origin)
+            {
+                var x = ReadDouble();
+
+                var y = ReadDouble();
+
+                return new Point(origin.X + x, origin.Y + y);
+            }
 
-            while ((i = reader.Peek()) != -1)
+            private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative)
             {
-                char c = (char)i;
+                ReadWhitespace(reader);
 
-                if (char.IsWhiteSpace(c))
+                var i = reader.Peek();
+
+                if (i == -1)
                 {
-                    reader.Read();
+                    return false;
                 }
-                else
+
+                var c = (char)i;
+
+                if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next))
                 {
-                    break;
+                    throw new InvalidDataException("Unexpected path command '" + c + "'.");
+                }
+
+                command = next;
+
+                relative = char.IsLower(c);
+
+                reader.Read();
+
+                return true;
+            }
+
+            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;
+                    }
                 }
             }
         }

+ 6 - 5
src/Avalonia.Visuals/Media/StreamGeometry.cs

@@ -35,14 +35,15 @@ namespace Avalonia.Media
         /// <returns>A <see cref="StreamGeometry"/>.</returns>
         public static new StreamGeometry Parse(string s)
         {
-            StreamGeometry result = new StreamGeometry();
+            var streamGeometry = new StreamGeometry();
 
-            using (StreamGeometryContext ctx = result.Open())
-            {
-                PathMarkupParser parser = new PathMarkupParser(ctx);
+            using (var context = streamGeometry.Open())
+            using (var parser = new PathMarkupParser(context))
+            {               
                 parser.Parse(s);
-                return result;
             }
+
+            return streamGeometry;
         }
 
         /// <inheritdoc/>

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

@@ -15,7 +15,7 @@ namespace Avalonia.Media
     /// <see cref="StreamGeometry.Open"/>.
     /// </remarks>
     /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed?
-    public class StreamGeometryContext : IDisposable
+    public class StreamGeometryContext : IGeometryContext
     {
         private readonly IStreamGeometryContextImpl _impl;
 

+ 66 - 0
src/Avalonia.Visuals/Platform/IGeometryContext.cs

@@ -0,0 +1,66 @@
+// 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 Avalonia.Media;
+
+namespace Avalonia.Platform
+{
+    /// <summary>
+    /// Describes a geometry using drawing commands.
+    /// </summary>
+    public interface IGeometryContext : IDisposable
+    {
+        /// <summary>
+        /// Draws an arc to the specified point.
+        /// </summary>
+        /// <param name="point">The destination point.</param>
+        /// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
+        /// <param name="rotationAngle">The rotation angle of the oval that specifies the curve.</param>
+        /// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
+        /// <param name="sweepDirection">
+        /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
+        /// </param>
+        void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
+
+        /// <summary>
+        /// Begins a new figure.
+        /// </summary>
+        /// <param name="startPoint">The starting point for the figure.</param>
+        /// <param name="isFilled">Whether the figure is filled.</param>
+        void BeginFigure(Point startPoint, bool isFilled = true);
+
+        /// <summary>
+        /// Draws a Bezier curve to the specified point.
+        /// </summary>
+        /// <param name="point1">The first control point used to specify the shape of the curve.</param>
+        /// <param name="point2">The second control point used to specify the shape of the curve.</param>
+        /// <param name="point3">The destination point for the end of the curve.</param>
+        void CubicBezierTo(Point point1, Point point2, Point point3);
+
+        /// <summary>
+        /// Draws a quadratic Bezier curve to the specified point
+        /// </summary>
+        /// <param name="control">Control point</param>
+        /// <param name="endPoint">DestinationPoint</param>
+        void QuadraticBezierTo(Point control, Point endPoint);
+
+        /// <summary>
+        /// Draws a line to the specified point.
+        /// </summary>
+        /// <param name="point">The destination point.</param>
+        void LineTo(Point point);
+
+        /// <summary>
+        /// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.
+        /// </summary>
+        /// <param name="isClosed">Whether the figure is closed.</param>
+        void EndFigure(bool isClosed);
+
+        /// <summary>
+        /// Sets the fill rule.
+        /// </summary>
+        /// <param name="fillRule">The fill rule.</param>
+        void SetFillRule(FillRule fillRule);
+    }
+}

+ 2 - 52
src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs

@@ -1,62 +1,12 @@
 // 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 Avalonia.Media;
-
 namespace Avalonia.Platform
 {
     /// <summary>
     /// Describes a geometry using drawing commands.
     /// </summary>
-    public interface IStreamGeometryContextImpl : IDisposable
-    {
-        /// <summary>
-        /// Draws an arc to the specified point.
-        /// </summary>
-        /// <param name="point">The destination point.</param>
-        /// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
-        /// <param name="rotationAngle">The rotation angle of the oval that specifies the curve.</param>
-        /// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
-        /// <param name="sweepDirection">
-        /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
-        /// </param>
-        void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
-
-        /// <summary>
-        /// Begins a new figure.
-        /// </summary>
-        /// <param name="startPoint">The starting point for the figure.</param>
-        /// <param name="isFilled">Whether the figure is filled.</param>
-        void BeginFigure(Point startPoint, bool isFilled);
-
-        /// <summary>
-        /// Draws a Bezier curve to the specified point.
-        /// </summary>
-        /// <param name="point1">The first control point used to specify the shape of the curve.</param>
-        /// <param name="point2">The second control point used to specify the shape of the curve.</param>
-        /// <param name="point3">The destination point for the end of the curve.</param>
-        void CubicBezierTo(Point point1, Point point2, Point point3);
-
-        /// <summary>
-        /// Draws a quadratic Bezier curve to the specified point
-        /// </summary>
-        /// <param name="control">Control point</param>
-        /// <param name="endPoint">DestinationPoint</param>
-        void QuadraticBezierTo(Point control, Point endPoint);
-
-        /// <summary>
-        /// Draws a line to the specified point.
-        /// </summary>
-        /// <param name="point">The destination point.</param>
-        void LineTo(Point point);
-
-        /// <summary>
-        /// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.
-        /// </summary>
-        /// <param name="isClosed">Whether the figure is closed.</param>
-        void EndFigure(bool isClosed);
-
-        void SetFillRule(FillRule fillRule);
+    public interface IStreamGeometryContextImpl : IGeometryContext
+    {      
     }
 }

+ 85 - 0
src/Avalonia.Visuals/Platform/PathGeometryContext.cs

@@ -0,0 +1,85 @@
+// 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 Avalonia.Media;
+using Avalonia.Platform;
+using System;
+
+namespace Avalonia.Visuals.Platform
+{
+    public class PathGeometryContext : IGeometryContext
+    {
+        private PathFigure _currentFigure;
+        private PathGeometry _pathGeometry;
+
+        public PathGeometryContext(PathGeometry pathGeometry)
+        {
+            _pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry));
+        }
+
+        public void Dispose()
+        {
+            _pathGeometry = null;
+        }
+
+        public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+        {
+            var arcSegment = new ArcSegment
+            {
+                Size = size,
+                RotationAngle = rotationAngle,
+                IsLargeArc = isLargeArc,
+                SweepDirection = sweepDirection,
+                Point = point
+            };
+
+            _currentFigure.Segments.Add(arcSegment);
+        }
+
+        public void BeginFigure(Point startPoint, bool isFilled)
+        {
+            _currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled };
+
+            _pathGeometry.Figures.Add(_currentFigure);
+        }
+
+        public void CubicBezierTo(Point point1, Point point2, Point point3)
+        {
+            var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 };
+
+            _currentFigure.Segments.Add(bezierSegment);
+        }
+
+        public void QuadraticBezierTo(Point control, Point endPoint)
+        {
+            var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint };
+
+            _currentFigure.Segments.Add(quadraticBezierSegment);
+        }
+
+        public void LineTo(Point point)
+        {
+            var lineSegment = new LineSegment
+            {
+                Point = point
+            };
+
+            _currentFigure.Segments.Add(lineSegment);
+        }
+
+        public void EndFigure(bool isClosed)
+        {
+            if (_currentFigure != null)
+            {
+                _currentFigure.IsClosed = isClosed;
+            }
+
+            _currentFigure = null;
+        }
+
+        public void SetFillRule(FillRule fillRule)
+        {
+            _pathGeometry.FillRule = fillRule;
+        }
+    }
+}

+ 207 - 106
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1194,16 +1194,6 @@ namespace Avalonia.Win32.Interop
             public IntPtr hIconSm;
         }
 
-        [Flags]
-        public enum OpenFileNameFlags
-        {
-            OFN_ALLOWMULTISELECT = 0x00000200,
-            OFN_EXPLORER = 0x00080000,
-            OFN_HIDEREADONLY = 0x00000004,
-            OFN_NOREADONLYRETURN = 0x00008000,
-            OFN_OVERWRITEPROMPT = 0x00000002
-        }
-
         public enum HRESULT : uint
         {
             S_FALSE = 0x0001,
@@ -1223,7 +1213,7 @@ namespace Avalonia.Win32.Interop
         public const uint SIGDN_FILESYSPATH = 0x80058000;
 
         [Flags]
-        internal enum FOS : uint
+        public enum FOS : uint
         {
             FOS_OVERWRITEPROMPT = 0x00000002,
             FOS_STRICTFILETYPES = 0x00000004,
@@ -1247,135 +1237,246 @@ namespace Avalonia.Win32.Interop
             FOS_DEFAULTNOMINIMODE = 0x20000000
         }
 
-        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
-        public struct OpenFileName
+        public static class ShellIds
         {
-            public int lStructSize;
-            public IntPtr hwndOwner;
-            public IntPtr hInstance;
-            public IntPtr lpstrFilter;
-            public IntPtr lpstrCustomFilter;
-            public int nMaxCustFilter;
-            public int nFilterIndex;
-            public IntPtr lpstrFile;
-            public int nMaxFile;
-            public IntPtr lpstrFileTitle;
-            public int nMaxFileTitle;
-            public IntPtr lpstrInitialDir;
-            public IntPtr lpstrTitle;
-            public OpenFileNameFlags Flags;
-            private readonly ushort Unused;
-            private readonly ushort Unused2;
-            public IntPtr lpstrDefExt;
-            public IntPtr lCustData;
-            public IntPtr lpfnHook;
-            public IntPtr lpTemplateName;
-            public IntPtr reservedPtr;
-            public int reservedInt;
-            public int flagsEx;
-        }        
-    }
+            public static readonly Guid OpenFileDialog = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7");
+            public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B");
+            public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8");
+            public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE");
+        }
 
-    [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
-    internal interface IFileDialog
-    {
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        [PreserveSig()]
-        uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow 
+        [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+        public interface IFileDialog
+        {
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            [PreserveSig()]
+            uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow
 
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFileTypes(uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] COMDLG_FILTERSPEC[] rgFilterSpec);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetFileTypeIndex([In] uint iFileType);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFileTypeIndex([In] uint iFileType);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetFileTypeIndex(out uint piFileType);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetFileTypeIndex(out uint piFileType);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint Unadvise([In] uint dwCookie);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint Unadvise([In] uint dwCookie);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetOptions([In] uint fos);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetOptions([In] uint fos);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetOptions(out uint fos);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetOptions(out uint fos);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint Close([MarshalAs(UnmanagedType.Error)] uint hr);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint Close([MarshalAs(UnmanagedType.Error)] uint hr);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetClientGuid([In] ref Guid guid);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetClientGuid([In] ref Guid guid);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint ClearClientData();
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint ClearClientData();
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
 
-    }
+        }
 
+        [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+        public interface IFileOpenDialog
+        {
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            [PreserveSig()]
+            uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow 
 
-    [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
-    internal interface IShellItem
-    {
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetFileTypeIndex([In] uint iFileType);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetFileTypeIndex(out uint piFileType);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs);
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
 
-        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
-        uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
-        
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void Unadvise([In] uint dwCookie);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint SetOptions([In] uint fos);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetOptions(out uint fos);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void Close([MarshalAs(UnmanagedType.Error)] int hr);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetClientGuid([In] ref Guid guid);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void ClearClientData();
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai);
+        }
+
+        [ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+        public interface IShellItemArray
+        {
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid,
+                         [In] ref Guid riid, out IntPtr ppvOut);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetPropertyDescriptionList([In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetCount(out uint pdwNumItems);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems);
+        }
+
+        [StructLayout(LayoutKind.Sequential, Pack = 4)]
+        public struct PROPERTYKEY
+        {
+            public Guid fmtid;
+            public uint pid;
+        }
+
+        public enum SIATTRIBFLAGS
+        {
+            SIATTRIBFLAGS_AND = 1,
+            SIATTRIBFLAGS_APPCOMPAT = 3,
+            SIATTRIBFLAGS_OR = 2
+        }
+
+        [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+        public interface IShellItem
+        {
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs);
+
+            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
+            uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
+
+        }
+
+        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+        public struct COMDLG_FILTERSPEC
+        {
+            [MarshalAs(UnmanagedType.LPWStr)]
+            public string pszName;
+            [MarshalAs(UnmanagedType.LPWStr)]
+            public string pszSpec;
+        }
     }
-    
+
     [Flags]
     internal enum DropEffect : int
     {

+ 88 - 140
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@@ -1,152 +1,94 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Platform;
+using Avalonia.Win32.Interop;
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
 using System.Linq;
 using System.Runtime.InteropServices;
-using System.Text;
 using System.Threading.Tasks;
-using Avalonia.Controls;
-using Avalonia.Controls.Platform;
-using Avalonia.Platform;
-using Avalonia.Win32.Interop;
 
 namespace Avalonia.Win32
 {
 
     class SystemDialogImpl : ISystemDialogImpl
     {
-        static char[] ToChars(string s)
-        {
-            if (s == null)
-                return null;
-            var chars = new char[s.Length];
-            for (int c = 0; c < s.Length; c++)
-                chars[c] = s[c];
-            return chars;
-        }
-
         public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
         {
             var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
             return Task.Factory.StartNew(() =>
             {
-                var filters = new StringBuilder();
-                foreach (var filter in dialog.Filters)
-                {
-                    var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e));
-                    filters.Append(filter.Name);
-                    filters.Append(" (");
-                    filters.Append(extMask);
-                    filters.Append(")");
-                    filters.Append('\0');
-                    filters.Append(extMask);
-                    filters.Append('\0');
-                }
-                if (filters.Length == 0)
-                    filters.Append("All files\0*.*\0");
-                filters.Append('\0');
+                var result = new string[0];
 
-                var filterBuffer = new char[filters.Length];
-                filters.CopyTo(0, filterBuffer, 0, filterBuffer.Length);
-
-                var defExt = ToChars((dialog as SaveFileDialog)?.DefaultExtension);
-                var fileBuffer = new char[256];
-                dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length);
+                Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
+                Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
+                UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk);
+                var frm = (UnmanagedMethods.IFileDialog)unk;
 
-                string userSelectedExt = string.Empty;
+                var openDialog = dialog as OpenFileDialog;
 
+                uint options;
+                frm.GetOptions(out options);
+                options |= (uint)(UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT);
+                if (openDialog?.AllowMultiple == true)
+                    options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT;
+                frm.SetOptions(options);
 
-                var title = ToChars(dialog.Title);
-                var initialDir = ToChars(dialog.InitialDirectory);
+                var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";
+                frm.SetDefaultExtension(defaultExtension);
+                frm.SetFileName(dialog.InitialFileName ?? "");
+                frm.SetTitle(dialog.Title);
 
-                fixed (char* pFileBuffer = fileBuffer)
-                fixed (char* pFilterBuffer = filterBuffer)
-                fixed (char* pDefExt = defExt)
-                fixed (char* pInitDir = initialDir)
-                fixed (char* pTitle = title)
+                var filters = new List<UnmanagedMethods.COMDLG_FILTERSPEC>();
+                foreach (var filter in dialog.Filters)
                 {
-                    var ofn = new UnmanagedMethods.OpenFileName()
-                    {
-                        hwndOwner = hWnd,
-                        hInstance = IntPtr.Zero,
-                        lCustData = IntPtr.Zero,
-                        nFilterIndex = 0,
-                        Flags =
-                            UnmanagedMethods.OpenFileNameFlags.OFN_EXPLORER |
-                            UnmanagedMethods.OpenFileNameFlags.OFN_HIDEREADONLY,
-                        nMaxCustFilter = 0,
-                        nMaxFile = fileBuffer.Length - 1,
-                        nMaxFileTitle = 0,
-                        lpTemplateName = IntPtr.Zero,
-                        lpfnHook = IntPtr.Zero,
-                        lpstrCustomFilter = IntPtr.Zero,
-                        lpstrDefExt = new IntPtr(pDefExt),
-                        lpstrFile = new IntPtr(pFileBuffer),
-                        lpstrFileTitle = IntPtr.Zero,
-                        lpstrFilter = new IntPtr(pFilterBuffer),
-                        lpstrInitialDir = new IntPtr(pInitDir),
-                        lpstrTitle = new IntPtr(pTitle),
-
-                    };
-                    ofn.lStructSize = Marshal.SizeOf(ofn);
-                    if ((dialog as OpenFileDialog)?.AllowMultiple == true)
-                        ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_ALLOWMULTISELECT;
-
-                    if (dialog is SaveFileDialog)
-                        ofn.Flags |= UnmanagedMethods.OpenFileNameFlags.OFN_NOREADONLYRETURN |
-                                     UnmanagedMethods.OpenFileNameFlags.OFN_OVERWRITEPROMPT;
-
-                    var pofn = &ofn;
-
-                    // We should save the current directory to restore it later.
-                    var currentDirectory = Environment.CurrentDirectory;
-
-                    var res = dialog is OpenFileDialog
-                        ? UnmanagedMethods.GetOpenFileName(new IntPtr(pofn))
-                        : UnmanagedMethods.GetSaveFileName(new IntPtr(pofn));
-
-                    // Restore the old current directory, since GetOpenFileName and GetSaveFileName change it after they're called
-                    Environment.CurrentDirectory = currentDirectory;
-
-                    if (!res)
-                        return null;
-                    if (dialog?.Filters.Count > 0)
-                        userSelectedExt = dialog.Filters[ofn.nFilterIndex - 1].Extensions.FirstOrDefault();
+                    var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e));
+                    filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask });
                 }
-                var cStart = 0;
-                string dir = null;
-                var files = new List<string>();
-                for (var c = 0; c < fileBuffer.Length; c++)
+                if (filters.Count == 0)
+                    filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" });
+
+                frm.SetFileTypes((uint)filters.Count, filters.ToArray());
+                frm.SetFileTypeIndex(0);
+
+                if (dialog.InitialDirectory != null)
                 {
-                    if (fileBuffer[c] == 0)
+                    UnmanagedMethods.IShellItem directoryShellItem;
+                    Guid riid = UnmanagedMethods.ShellIds.IShellItem;
+                    if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
                     {
-                        //Encountered double zero char
-                        if (cStart == c)
-                            break;
-
-                        var s = new string(fileBuffer, cStart, c - cStart);
-                        if (dir == null)
-                            dir = s;
-                        else
-                            files.Add(s);
-                        cStart = c + 1;
+                        frm.SetFolder(directoryShellItem);
+                        frm.SetDefaultFolder(directoryShellItem);
                     }
                 }
-                if (files.Count == 0)
+
+                if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK)
                 {
-                    if (dialog is SaveFileDialog)
+                    if (openDialog?.AllowMultiple == true)
                     {
-                        if (string.IsNullOrWhiteSpace(Path.GetExtension(dir)) &&
-                            !string.IsNullOrWhiteSpace(userSelectedExt) &&
-                            !userSelectedExt.Contains("*"))
-                            dir = Path.ChangeExtension(dir, userSelectedExt);
+                        UnmanagedMethods.IShellItemArray shellItemArray;
+                        ((UnmanagedMethods.IFileOpenDialog)frm).GetResults(out shellItemArray);
+                        uint count;
+                        shellItemArray.GetCount(out count);
+                        result = new string[count];
+                        for (uint i = 0; i < count; i++)
+                        {
+                            UnmanagedMethods.IShellItem shellItem;
+                            shellItemArray.GetItemAt(i, out shellItem);
+                            result[i] = GetAbsoluteFilePath(shellItem);
+                        }
+                    }
+                    else
+                    {
+                        UnmanagedMethods.IShellItem shellItem;
+                        if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
+                        {
+                            result = new string[] { GetAbsoluteFilePath(shellItem) };
+                        }
                     }
-
-                    return new[] { dir };
                 }
 
-                return files.Select(f => Path.Combine(dir, f)).ToArray();
+                return result;
             });
         }
 
@@ -157,11 +99,11 @@ namespace Avalonia.Win32
                 string result = string.Empty;
 
                 var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
-                var clsid = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7");
-                var iid  = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8");
+                Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
+                Guid iid  = UnmanagedMethods.ShellIds.IFileDialog;
 
                 UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk);
-                var frm = (IFileDialog)unk;
+                var frm = (UnmanagedMethods.IFileDialog)unk;
                 uint options;
                 frm.GetOptions(out options);
                 options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT);
@@ -169,8 +111,8 @@ namespace Avalonia.Win32
 
                 if (dialog.InitialDirectory != null)
                 {
-                    IShellItem directoryShellItem;
-                    var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem
+                    UnmanagedMethods.IShellItem directoryShellItem;
+                    Guid riid = UnmanagedMethods.ShellIds.IShellItem;
                     if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
                     {
                         frm.SetFolder(directoryShellItem);
@@ -179,8 +121,8 @@ namespace Avalonia.Win32
 
                 if (dialog.DefaultDirectory != null)
                 {
-                    IShellItem directoryShellItem;
-                    var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem
+                    UnmanagedMethods.IShellItem directoryShellItem;
+                    Guid riid = UnmanagedMethods.ShellIds.IShellItem;
                     if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
                     {
                         frm.SetDefaultFolder(directoryShellItem);
@@ -189,29 +131,35 @@ namespace Avalonia.Win32
 
                 if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK)
                 {
-                    IShellItem shellItem;
+                    UnmanagedMethods.IShellItem shellItem;
                     if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
                     {
-                        IntPtr pszString;
-                        if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK)
-                        {
-                            if (pszString != IntPtr.Zero)
-                            {
-                                try
-                                {
-                                    result = Marshal.PtrToStringAuto(pszString);
-                                }
-                                finally
-                                {
-                                    Marshal.FreeCoTaskMem(pszString);
-                                }
-                            }
-                        }
+                        result = GetAbsoluteFilePath(shellItem);
                     }
                 }
 
                 return result;
             });
         }
+
+        private string GetAbsoluteFilePath(UnmanagedMethods.IShellItem shellItem)
+        {
+            IntPtr pszString;
+            if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK)
+            {
+                if (pszString != IntPtr.Zero)
+                {
+                    try
+                    {
+                        return Marshal.PtrToStringAuto(pszString);
+                    }
+                    finally
+                    {
+                        Marshal.FreeCoTaskMem(pszString);
+                    }
+                }
+            }
+            return "";
+        }
     }
 }

+ 3 - 2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFrameworks>netcoreapp2.0</TargetFrameworks>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <OutputType>Exe</OutputType>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
@@ -15,7 +16,7 @@
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.10.14" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Properties\" />

+ 45 - 0
tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs

@@ -0,0 +1,45 @@
+// 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 Avalonia.Media;
+using Avalonia.UnitTests;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Visuals.Media
+{
+    [MemoryDiagnoser]
+    public class PathMarkupParserTests : IDisposable
+    {
+        private IDisposable _app;
+
+        public PathMarkupParserTests()
+        {
+            _app = UnitTestApplication.Start(TestServices.StyledWindow);
+        }
+
+        public void Dispose()
+        {
+            _app.Dispose();
+        }
+
+        [Benchmark]
+        public void Parse_Large_Path()
+        {
+            const string PathData = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" +
+                                " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" +
+                                "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" +
+                                " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" +
+                                ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" +
+                                ".9063 10.1309 30.9062 16.6309 19.9063 Z ";
+
+            var streamGeometry = new StreamGeometry();
+
+            using (var context = streamGeometry.Open())
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse(PathData);
+            }
+        }
+    }
+}

+ 108 - 34
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@@ -2,8 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using Avalonia.Media;
-using Avalonia.Platform;
-using Moq;
+using Avalonia.Visuals.Platform;
 using Xunit;
 
 namespace Avalonia.Visuals.UnitTests.Media
@@ -13,48 +12,133 @@ namespace Avalonia.Visuals.UnitTests.Media
         [Fact]
         public void Parses_Move()
         {
-            using (AvaloniaLocator.EnterScope())
-            {
-                var result = new Mock<IStreamGeometryContextImpl>();
-
-                var parser = PrepareParser(result);
-
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {               
                 parser.Parse("M10 10");
 
-                result.Verify(x => x.BeginFigure(new Point(10, 10), true));
+                var figure = pathGeometry.Figures[0];
+
+                Assert.Equal(new Point(10, 10), figure.StartPoint);
             }
         }
 
         [Fact]
         public void Parses_Line()
         {
-            using (AvaloniaLocator.EnterScope())
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
             {
-                var result = new Mock<IStreamGeometryContextImpl>();
+                parser.Parse("M0 0L10 10");
 
-                var parser = PrepareParser(result);
+                var figure = pathGeometry.Figures[0];
 
-                parser.Parse("M0 0L10 10");
+                var segment = figure.Segments[0];
+
+                Assert.IsType<LineSegment>(segment);
 
-                result.Verify(x => x.LineTo(new Point(10, 10)));
+                var lineSegment = (LineSegment)segment;
+
+                Assert.Equal(new Point(10, 10), lineSegment.Point);
             }
         }
 
         [Fact]
         public void Parses_Close()
         {
-            using (AvaloniaLocator.EnterScope())
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
             {
-                var result = new Mock<IStreamGeometryContextImpl>();
+                parser.Parse("M0 0L10 10z");
 
-                var parser = PrepareParser(result);
+                var figure = pathGeometry.Figures[0];
 
-                parser.Parse("M0 0L10 10z");
+                Assert.True(figure.IsClosed);
+            }
+        }
 
-                result.Verify(x => x.EndFigure(true));
+        [Fact]
+        public void Parses_FillMode_Before_Move()
+        {
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse("F 1M0,0");             
+
+                Assert.Equal(FillRule.NonZero, pathGeometry.FillRule);
             }
         }
 
+        [Theory]
+        [InlineData("M0 0 10 10 20 20")]
+        [InlineData("M0,0 10,10 20,20")]
+        [InlineData("M0,0,10,10,20,20")]
+        public void Parses_Implicit_Line_Command_After_Move(string pathData)
+        {
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse(pathData);
+
+                var figure = pathGeometry.Figures[0];
+
+                var segment = figure.Segments[0];
+
+                Assert.IsType<LineSegment>(segment);
+
+                var lineSegment = (LineSegment)segment;
+
+                Assert.Equal(new Point(10, 10), lineSegment.Point);
+
+                figure = pathGeometry.Figures[1];
+
+                segment = figure.Segments[0];
+
+                Assert.IsType<LineSegment>(segment);
+
+                lineSegment = (LineSegment)segment;
+
+                Assert.Equal(new Point(20, 20), lineSegment.Point);
+            }
+        }
+
+        [Theory]
+        [InlineData("m0 0 10 10 20 20")]
+        [InlineData("m0,0 10,10 20,20")]
+        [InlineData("m0,0,10,10,20,20")]
+        public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData)
+        {
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
+            {
+                parser.Parse(pathData);
+
+                var figure = pathGeometry.Figures[0];
+
+                var segment = figure.Segments[0];
+
+                Assert.IsType<LineSegment>(segment);
+
+                var lineSegment = (LineSegment)segment;
+
+                Assert.Equal(new Point(10, 10), lineSegment.Point);
+
+                segment = figure.Segments[1];
+
+                Assert.IsType<LineSegment>(segment);
+
+                lineSegment = (LineSegment)segment;
+
+                Assert.Equal(new Point(30, 30), lineSegment.Point);
+            }
+        }       
+
         [Theory]
         [InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107
         [InlineData("M0 0L10 10z")]
@@ -75,29 +159,19 @@ namespace Avalonia.Visuals.UnitTests.Media
             ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" +
             ".9063 10.1309 30.9062 16.6309 19.9063 Z ")]
         [InlineData(
-            "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " + 
-            "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " + 
+            "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " +
+            "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " +
             "12.461,8.046C14.45,8.278,16,9.949,16,12")]
         public void Should_Parse(string pathData)
         {
-            using (AvaloniaLocator.EnterScope())
+            var pathGeometry = new PathGeometry();
+            using (var context = new PathGeometryContext(pathGeometry))
+            using (var parser = new PathMarkupParser(context))
             {
-                var parser = PrepareParser();
-
                 parser.Parse(pathData);
 
                 Assert.True(true);
             }
         }
-
-        private static PathMarkupParser PrepareParser(Mock<IStreamGeometryContextImpl> implMock = null)
-        {
-            AvaloniaLocator.CurrentMutable
-                    .Bind<IPlatformRenderInterface>()
-                    .ToConstant(Mock.Of<IPlatformRenderInterface>());
-
-            return new PathMarkupParser(
-                new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of<IStreamGeometryContextImpl>()));
-        }
     }
 }