diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderTest/MainWindow.xaml index 1d3001f1b1..9e9a600161 100644 --- a/samples/RenderTest/MainWindow.xaml +++ b/samples/RenderTest/MainWindow.xaml @@ -27,6 +27,7 @@ + \ No newline at end of file diff --git a/samples/RenderTest/Pages/DrawingPage.xaml b/samples/RenderTest/Pages/DrawingPage.xaml new file mode 100644 index 0000000000..81181e01fc --- /dev/null +++ b/samples/RenderTest/Pages/DrawingPage.xaml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/RenderTest/Pages/DrawingPage.xaml.cs b/samples/RenderTest/Pages/DrawingPage.xaml.cs new file mode 100644 index 0000000000..3bf9bd545d --- /dev/null +++ b/samples/RenderTest/Pages/DrawingPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace RenderTest.Pages +{ + public class DrawingPage : UserControl + { + public DrawingPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderTest/RenderTest.csproj index ea5c0bcc58..b7e64f4dae 100644 --- a/samples/RenderTest/RenderTest.csproj +++ b/samples/RenderTest/RenderTest.csproj @@ -51,6 +51,9 @@ App.xaml + + DrawingPage.xaml + ClippingPage.xaml @@ -178,6 +181,11 @@ Designer + + + Designer + + diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs new file mode 100644 index 0000000000..af3665fabc --- /dev/null +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -0,0 +1,59 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Metadata; + +namespace Avalonia.Controls +{ + public class DrawingPresenter : Control + { + static DrawingPresenter() + { + AffectsMeasure(DrawingProperty); + AffectsRender(DrawingProperty); + } + + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + public static readonly StyledProperty StretchProperty = + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + public Stretch Stretch + { + get => GetValue(StretchProperty); + set => SetValue(StretchProperty, value); + } + + private Matrix _transform = Matrix.Identity; + + protected override Size MeasureOverride(Size availableSize) + { + if (Drawing == null) return new Size(); + + var (size, transform) = Shape.CalculateSizeAndTransform(availableSize, Drawing.GetBounds(), Stretch); + + _transform = transform; + + return size; + } + + public override void Render(DrawingContext context) + { + if (Drawing != null) + { + using (context.PushPreTransform(_transform)) + using (context.PushClip(Bounds)) + { + Drawing.Draw(context); + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 427749263a..c03f4dc563 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -155,11 +155,21 @@ namespace Avalonia.Controls.Shapes { // This should probably use GetRenderBounds(strokeThickness) but then the calculations // will multiply the stroke thickness as well, which isn't correct. - Rect shapeBounds = DefiningGeometry.Bounds; + var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); + + if (_transform != transform) + { + _transform = transform; + _renderedGeometry = null; + } + + return size; + } + + internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) + { Size shapeSize = new Size(shapeBounds.Right, shapeBounds.Bottom); Matrix translate = Matrix.Identity; - double width = Width; - double height = Height; double desiredX = availableSize.Width; double desiredY = availableSize.Height; double sx = 0.0; @@ -226,15 +236,9 @@ namespace Avalonia.Controls.Shapes break; } - var t = translate * Matrix.CreateScale(sx, sy); - - if (_transform != t) - { - _transform = t; - _renderedGeometry = null; - } - - return new Size(shapeSize.Width * sx, shapeSize.Height * sy); + var transform = translate * Matrix.CreateScale(sx, sy); + var size = new Size(shapeSize.Width * sx, shapeSize.Height * sy); + return (size, transform); } private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 8812000bdc..10549b967d 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Linq; namespace Avalonia { @@ -295,5 +296,33 @@ namespace Avalonia ((_m21 * _m32) - (_m22 * _m31)) / d, ((_m12 * _m31) - (_m11 * _m32)) / d); } + + /// + /// Parses a string. + /// + /// The string. + /// The current culture. + /// The . + public static Matrix Parse(string s, CultureInfo culture) + { + var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToArray(); + + if (parts.Length == 6) + { + return new Matrix( + double.Parse(parts[0], culture), + double.Parse(parts[1], culture), + double.Parse(parts[2], culture), + double.Parse(parts[3], culture), + double.Parse(parts[4], culture), + double.Parse(parts[5], culture)); + } + else + { + throw new FormatException("Invalid Matrix."); + } + } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/Drawing.cs b/src/Avalonia.Visuals/Media/Drawing.cs new file mode 100644 index 0000000000..a60c591edc --- /dev/null +++ b/src/Avalonia.Visuals/Media/Drawing.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Media +{ + public abstract class Drawing : AvaloniaObject + { + public abstract void Draw(DrawingContext context); + + public abstract Rect GetBounds(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs new file mode 100644 index 0000000000..744ff2af03 --- /dev/null +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -0,0 +1,58 @@ +using Avalonia.Collections; +using Avalonia.Metadata; + +namespace Avalonia.Media +{ + public class DrawingGroup : Drawing + { + public static readonly StyledProperty OpacityProperty = + AvaloniaProperty.Register(nameof(Opacity), 1); + + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + + public double Opacity + { + get => GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } + + public Transform Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + + [Content] + public AvaloniaList Children { get; } = new AvaloniaList(); + + public override void Draw(DrawingContext context) + { + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) + using (context.PushOpacity(Opacity)) + { + foreach (var drawing in Children) + { + drawing.Draw(context); + } + } + } + + public override Rect GetBounds() + { + var rect = new Rect(); + + foreach (var drawing in Children) + { + rect = rect.Union(drawing.GetBounds()); + } + + if (Transform != null) + { + rect = rect.TransformToAABB(Transform.Value); + } + + return rect; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/EllipseGeometry.cs b/src/Avalonia.Visuals/Media/EllipseGeometry.cs index 414fd9ab59..591b55cf58 100644 --- a/src/Avalonia.Visuals/Media/EllipseGeometry.cs +++ b/src/Avalonia.Visuals/Media/EllipseGeometry.cs @@ -11,16 +11,51 @@ namespace Avalonia.Media /// public class EllipseGeometry : Geometry { + /// + /// Defines the property. + /// + public static readonly StyledProperty RectProperty = + AvaloniaProperty.Register(nameof(Rect)); + + public Rect Rect + { + get => GetValue(RectProperty); + set => SetValue(RectProperty, value); + } + + static EllipseGeometry() + { + RectProperty.Changed.AddClassHandler(x => x.RectChanged); + } + /// /// Initializes a new instance of the class. /// - /// The rectangle that the ellipse should fill. - public EllipseGeometry(Rect rect) + public EllipseGeometry() { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - IStreamGeometryImpl impl = factory.CreateStreamGeometry(); + PlatformImpl = factory.CreateStreamGeometry(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The rectangle that the ellipse should fill. + public EllipseGeometry(Rect rect) : this() + { + Rect = rect; + } + + /// + public override Geometry Clone() + { + return new EllipseGeometry(Rect); + } - using (IStreamGeometryContextImpl ctx = impl.Open()) + private void RectChanged(AvaloniaPropertyChangedEventArgs e) + { + var rect = (Rect)e.NewValue; + using (var ctx = ((IStreamGeometryImpl)PlatformImpl).Open()) { double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3; var center = rect.Center; @@ -45,14 +80,6 @@ namespace Avalonia.Media ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0)); ctx.EndFigure(true); } - - PlatformImpl = impl; - } - - /// - public override Geometry Clone() - { - return new EllipseGeometry(Bounds); } } } diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs new file mode 100644 index 0000000000..e67e853a84 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -0,0 +1,43 @@ +namespace Avalonia.Media +{ + public class GeometryDrawing : Drawing + { + public static readonly StyledProperty GeometryProperty = + AvaloniaProperty.Register(nameof(Geometry)); + + public Geometry Geometry + { + get => GetValue(GeometryProperty); + set => SetValue(GeometryProperty, value); + } + + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); + + public IBrush Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); + + public Pen Pen + { + get => GetValue(PenProperty); + set => SetValue(PenProperty, value); + } + + public override void Draw(DrawingContext context) + { + context.DrawGeometry(Brush, Pen, Geometry); + } + + public override Rect GetBounds() + { + // adding the Pen's stroke thickness here could yield wrong results due to transforms + return Geometry?.GetRenderBounds(0) ?? new Rect(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/LineGeometry.cs b/src/Avalonia.Visuals/Media/LineGeometry.cs index 2783d7fb26..323bfa5a7e 100644 --- a/src/Avalonia.Visuals/Media/LineGeometry.cs +++ b/src/Avalonia.Visuals/Media/LineGeometry.cs @@ -10,35 +10,89 @@ namespace Avalonia.Media /// public class LineGeometry : Geometry { - private Point _startPoint; - private Point _endPoint; + /// + /// Defines the property. + /// + public static readonly StyledProperty StartPointProperty = + AvaloniaProperty.Register(nameof(StartPoint)); + + public Point StartPoint + { + get => GetValue(StartPointProperty); + set => SetValue(StartPointProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty EndPointProperty = + AvaloniaProperty.Register(nameof(EndPoint)); + private bool _isDirty; + + public Point EndPoint + { + get => GetValue(EndPointProperty); + set => SetValue(EndPointProperty, value); + } + + static LineGeometry() + { + StartPointProperty.Changed.AddClassHandler(x => x.PointsChanged); + EndPointProperty.Changed.AddClassHandler(x => x.PointsChanged); + } + + /// + /// Initializes a new instance of the class. + /// + public LineGeometry() + { + IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); + PlatformImpl = factory.CreateStreamGeometry(); + } /// /// Initializes a new instance of the class. /// /// The start point. /// The end point. - public LineGeometry(Point startPoint, Point endPoint) + public LineGeometry(Point startPoint, Point endPoint) : this() { - _startPoint = startPoint; - _endPoint = endPoint; - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - IStreamGeometryImpl impl = factory.CreateStreamGeometry(); + StartPoint = startPoint; + EndPoint = endPoint; + } - using (IStreamGeometryContextImpl context = impl.Open()) + public override IGeometryImpl PlatformImpl + { + get { - context.BeginFigure(_startPoint, false); - context.LineTo(_endPoint); - context.EndFigure(false); + PrepareIfNeeded(); + return base.PlatformImpl; } + protected set => base.PlatformImpl = value; + } - PlatformImpl = impl; + public void PrepareIfNeeded() + { + if (_isDirty) + { + _isDirty = false; + + using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) + { + context.BeginFigure(StartPoint, false); + context.LineTo(EndPoint); + context.EndFigure(false); + } + } } /// public override Geometry Clone() { - return new LineGeometry(_startPoint, _endPoint); + PrepareIfNeeded(); + return new LineGeometry(StartPoint, EndPoint); } + + private void PointsChanged(AvaloniaPropertyChangedEventArgs e) => _isDirty = true; } } diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9c5ffe7151..fad55ed531 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -141,6 +141,7 @@ namespace Avalonia.Media 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); diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index 709ad7a9f5..7c47e7d04d 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -3,10 +3,9 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Platform; +using Avalonia.Metadata; +using Avalonia.Collections; namespace Avalonia.Media { @@ -15,36 +14,118 @@ namespace Avalonia.Media /// public class PolylineGeometry : Geometry { - private IList _points; - private bool _isFilled; + /// + /// Defines the property. + /// + public static readonly DirectProperty PointsProperty = + AvaloniaProperty.RegisterDirect(nameof(Points), g => g.Points, (g, f) => g.Points = f); - public PolylineGeometry(IList points, bool isFilled) + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty IsFilledProperty = + AvaloniaProperty.Register(nameof(IsFilled)); + + private Points _points; + private bool _isDirty; + private IDisposable _pointsObserver; + + static PolylineGeometry() + { + PointsProperty.Changed.AddClassHandler((s, e) => + s.OnPointsChanged(e.OldValue as Points, e.NewValue as Points)); + IsFilledProperty.Changed.AddClassHandler((s, _) => s.NotifyChanged()); + } + + /// + /// Initializes a new instance of the class. + /// + public PolylineGeometry() { - _points = points; - _isFilled = isFilled; IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - IStreamGeometryImpl impl = factory.CreateStreamGeometry(); + PlatformImpl = factory.CreateStreamGeometry(); + + Points = new Points(); + } - using (IStreamGeometryContextImpl context = impl.Open()) + /// + /// Initializes a new instance of the class. + /// + public PolylineGeometry(IEnumerable points, bool isFilled) : this() + { + Points.AddRange(points); + IsFilled = isFilled; + } + + public void PrepareIfNeeded() + { + if (_isDirty) { - if (points.Count > 0) + _isDirty = false; + + using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) { - context.BeginFigure(points[0], isFilled); - for (int i = 1; i < points.Count; i++) + var points = Points; + var isFilled = IsFilled; + if (points.Count > 0) { - context.LineTo(points[i]); + context.BeginFigure(points[0], isFilled); + for (int i = 1; i < points.Count; i++) + { + context.LineTo(points[i]); + } + context.EndFigure(isFilled); } - context.EndFigure(isFilled); } } + } - PlatformImpl = impl; + /// + /// Gets or sets the figures. + /// + /// + /// The points. + /// + [Content] + public Points Points + { + get => _points; + set => SetAndRaise(PointsProperty, ref _points, value); + } + + public bool IsFilled + { + get => GetValue(IsFilledProperty); + set => SetValue(IsFilledProperty, value); + } + + public override IGeometryImpl PlatformImpl + { + get + { + PrepareIfNeeded(); + return base.PlatformImpl; + } + protected set => base.PlatformImpl = value; } /// public override Geometry Clone() { - return new PolylineGeometry(new List(_points), _isFilled); + PrepareIfNeeded(); + return new PolylineGeometry(Points, IsFilled); + } + + private void OnPointsChanged(Points oldValue, Points newValue) + { + _pointsObserver?.Dispose(); + + _pointsObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged()); + } + + internal void NotifyChanged() + { + _isDirty = true; } } } diff --git a/src/Avalonia.Visuals/Media/RectangleGeometry.cs b/src/Avalonia.Visuals/Media/RectangleGeometry.cs index ef7deaa6f6..1aa449d9e1 100644 --- a/src/Avalonia.Visuals/Media/RectangleGeometry.cs +++ b/src/Avalonia.Visuals/Media/RectangleGeometry.cs @@ -10,16 +10,51 @@ namespace Avalonia.Media /// public class RectangleGeometry : Geometry { + /// + /// Defines the property. + /// + public static readonly StyledProperty RectProperty = + AvaloniaProperty.Register(nameof(Rect)); + + public Rect Rect + { + get => GetValue(RectProperty); + set => SetValue(RectProperty, value); + } + + static RectangleGeometry() + { + RectProperty.Changed.AddClassHandler(x => x.RectChanged); + } + /// /// Initializes a new instance of the class. /// - /// The rectangle bounds. - public RectangleGeometry(Rect rect) + public RectangleGeometry() { IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - IStreamGeometryImpl impl = factory.CreateStreamGeometry(); + PlatformImpl = factory.CreateStreamGeometry(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The rectangle bounds. + public RectangleGeometry(Rect rect) : this() + { + Rect = rect; + } + + /// + public override Geometry Clone() + { + return new RectangleGeometry(Rect); + } - using (IStreamGeometryContextImpl context = impl.Open()) + private void RectChanged(AvaloniaPropertyChangedEventArgs e) + { + var rect = (Rect)e.NewValue; + using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) { context.BeginFigure(rect.TopLeft, true); context.LineTo(rect.TopRight); @@ -27,14 +62,6 @@ namespace Avalonia.Media context.LineTo(rect.BottomLeft); context.EndFigure(true); } - - PlatformImpl = impl; - } - - /// - public override Geometry Clone() - { - return new RectangleGeometry(Bounds); } } } diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 7c7a3336fc..5fbd082967 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -183,7 +183,7 @@ namespace Avalonia } else { - throw new FormatException("Invalid Thickness."); + throw new FormatException("Invalid Point."); } } diff --git a/src/Avalonia.Visuals/Points.cs b/src/Avalonia.Visuals/Points.cs new file mode 100644 index 0000000000..867d3d4d24 --- /dev/null +++ b/src/Avalonia.Visuals/Points.cs @@ -0,0 +1,9 @@ +// 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.Collections; + +namespace Avalonia +{ + public sealed class Points : AvaloniaList { } +} diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 0132c5e8a3..d562429fc7 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -481,5 +481,31 @@ namespace Avalonia _width, _height); } + + /// + /// Parses a string. + /// + /// The string. + /// The current culture. + /// The parsed . + public static Rect Parse(string s, CultureInfo culture) + { + var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToList(); + + if (parts.Count == 4) + { + return new Rect( + double.Parse(parts[0], culture), + double.Parse(parts[1], culture), + double.Parse(parts[2], culture), + double.Parse(parts[3], culture)); + } + else + { + throw new FormatException("Invalid Rect."); + } + } } } diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index 3ce3797c49..a11f080e94 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -203,7 +203,7 @@ namespace Avalonia } else { - throw new FormatException("Invalid Rect."); + throw new FormatException("Invalid RelativeRect."); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index dbf985fd79..08ea6b6877 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -31,6 +31,8 @@ + + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs new file mode 100644 index 0000000000..c477ff5637 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs @@ -0,0 +1,23 @@ +// 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.Globalization; + +namespace Avalonia.Markup.Xaml.Converters +{ + using System.ComponentModel; + + public class MatrixTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return Matrix.Parse((string)value, culture); + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs new file mode 100644 index 0000000000..c9c6462f89 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs @@ -0,0 +1,23 @@ +// 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.Globalization; + +namespace Avalonia.Markup.Xaml.Converters +{ + using System.ComponentModel; + + public class RectTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return Rect.Parse((string)value, culture); + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs index 66e9a697e4..1cf5b6a58e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs @@ -32,12 +32,14 @@ namespace Avalonia.Markup.Xaml.PortableXaml { typeof(AvaloniaList), typeof(AvaloniaListTypeConverter) }, { typeof(IMemberSelector), typeof(MemberSelectorTypeConverter) }, { typeof(Point), typeof(PointTypeConverter) }, + { typeof(Matrix), typeof(MatrixTypeConverter) }, { typeof(IList), typeof(PointsListTypeConverter) }, { typeof(AvaloniaProperty), typeof(AvaloniaPropertyTypeConverter) }, { typeof(RelativePoint), typeof(RelativePointTypeConverter) }, { typeof(RelativeRect), typeof(RelativeRectTypeConverter) }, { typeof(RowDefinitions), typeof(RowDefinitionsTypeConverter) }, { typeof(Size), typeof(SizeTypeConverter) }, + { typeof(Rect), typeof(RectTypeConverter) }, { typeof(Selector), typeof(SelectorTypeConverter)}, { typeof(SolidColorBrush), typeof(BrushTypeConverter) }, { typeof(Thickness), typeof(ThicknessTypeConverter) }, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs new file mode 100644 index 0000000000..4c1e361952 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs @@ -0,0 +1,16 @@ +using System.Globalization; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class MatrixTests + { + [Fact] + public void Parse_Parses() + { + var matrix = Matrix.Parse("1,2,3,-4,5 6", CultureInfo.CurrentCulture); + var expected = new Matrix(1, 2, 3, -4, 5, 6); + Assert.Equal(expected, matrix); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index f0d41680c0..3b903b4436 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -56,6 +56,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [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")] [InlineData("M50 50 L100 100 L150 50")] [InlineData("M50 50L100 100L150 50")] diff --git a/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs new file mode 100644 index 0000000000..12070bfed3 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs @@ -0,0 +1,16 @@ +using System.Globalization; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class RectTests + { + [Fact] + public void Parse_Parses() + { + var rect = Rect.Parse("1,2 3,-4", CultureInfo.CurrentCulture); + var expected = new Rect(1, 2, 3, -4); + Assert.Equal(expected, rect); + } + } +} \ No newline at end of file