From 7de42e02e1c5d6af042c3b4b97e842e4f9cec159 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 19 Aug 2017 17:11:07 +0200 Subject: [PATCH 01/10] Clear scene for invisible root visuals. When a root visual is hidden, clear `DeferredRenderer._scene` and completely rebuild it if it is shown again. Fixes #1096 --- .../Rendering/DeferredRenderer.cs | 42 +++++++++++-------- .../Rendering/SceneGraph/SceneBuilder.cs | 6 +++ .../SceneGraph/SceneBuilderTests_Layers.cs | 27 ------------ 3 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index d3d6776efa3..c30fb3bdc3f 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -11,6 +11,7 @@ using System.IO; using Avalonia.Media.Immutable; using System.Threading; +using System.Linq; namespace Avalonia.Rendering { @@ -58,7 +59,6 @@ public DeferredRenderer( _dispatcher = dispatcher ?? Dispatcher.UIThread; _root = root; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _scene = new Scene(root); _layerFactory = layerFactory ?? new DefaultRenderLayerFactory(); _layers = new RenderLayers(_layerFactory); _renderLoop = renderLoop; @@ -86,7 +86,6 @@ public DeferredRenderer( _root = root; _renderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _scene = new Scene(root); _layerFactory = layerFactory ?? new DefaultRenderLayerFactory(); _layers = new RenderLayers(_layerFactory); } @@ -122,7 +121,7 @@ public IEnumerable HitTest(Point p, Func filter) UpdateScene(); } - return _scene.HitTest(p, filter); + return _scene?.HitTest(p, filter) ?? Enumerable.Empty(); } /// @@ -186,7 +185,7 @@ private void Render(Scene scene) _dirtyRectsDisplay.Tick(); } - if (scene.Size != Size.Empty) + if (scene != null && scene.Size != Size.Empty) { if (scene.Generation != _lastSceneId) { @@ -366,25 +365,32 @@ private void UpdateScene() try { - var scene = _scene.Clone(); - - if (_dirty == null) - { - _dirty = new DirtyVisuals(); - _sceneBuilder.UpdateAll(scene); - } - else if (_dirty.Count > 0) + if (_root.IsVisible) { - foreach (var visual in _dirty) + var scene = _scene?.Clone() ?? new Scene(_root); + + if (_dirty == null) { - _sceneBuilder.Update(scene, visual); + _dirty = new DirtyVisuals(); + _sceneBuilder.UpdateAll(scene); + } + else if (_dirty.Count > 0) + { + foreach (var visual in _dirty) + { + _sceneBuilder.Update(scene, visual); + } } - } - Interlocked.Exchange(ref _scene, scene); + Interlocked.Exchange(ref _scene, scene); - _dirty.Clear(); - (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size)); + _dirty.Clear(); + (_root as IRenderRoot)?.Invalidate(new Rect(scene.Size)); + } + else + { + Interlocked.Exchange(ref _scene, null); + } } finally { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index afdf488b313..10455eb1470 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -36,8 +36,14 @@ public bool Update(Scene scene, IVisual visual) { Contract.Requires(scene != null); Contract.Requires(visual != null); + Dispatcher.UIThread.VerifyAccess(); + if (!scene.Root.Visual.IsVisible) + { + throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); + } + var node = (VisualNode)scene.FindNode(visual); if (visual == scene.Root.Visual) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index 13f28018db8..e65487ac44f 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs @@ -273,32 +273,5 @@ public void GeometryClip_Should_Affect_Child_Layers() ((MockStreamGeometryImpl)borderLayer.GeometryClip).Transform); } } - - [Fact] - public void Hiding_Root_Should_Not_Remove_Root_Layer() - { - using (TestApplication()) - { - Border border; - var tree = new TestRoot - { - Child = border = new Border() - }; - - var layout = AvaloniaLocator.Current.GetService(); - layout.ExecuteInitialLayoutPass(tree); - - var scene = new Scene(tree); - var sceneBuilder = new SceneBuilder(); - sceneBuilder.UpdateAll(scene); - - tree.IsVisible = false; - - scene = scene.Clone(); - sceneBuilder.Update(scene, tree); - - Assert.Equal(1, scene.Layers.Count); - } - } } } From 8cc6d3069638811929132d6ce583503c3d1706b8 Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 14:05:13 +0300 Subject: [PATCH 02/10] Matrix.Parse & type converter --- src/Avalonia.Visuals/Matrix.cs | 29 +++++++++++++++++++ src/Avalonia.Visuals/Point.cs | 2 +- .../Avalonia.Markup.Xaml.csproj | 1 + .../Converters/MatrixTypeConverter.cs | 23 +++++++++++++++ .../AvaloniaDefaultTypeConverters.cs | 1 + .../Media/MatrixTests.cs | 16 ++++++++++ 6 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Converters/MatrixTypeConverter.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 8812000bdc4..ee31ea91527 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 @@ public Matrix Invert() ((_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/Point.cs b/src/Avalonia.Visuals/Point.cs index 7c7a3336fc4..5fbd0829671 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.cs @@ -183,7 +183,7 @@ public static Point Parse(string s, CultureInfo culture) } else { - throw new FormatException("Invalid Thickness."); + throw new FormatException("Invalid Point."); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index dbf985fd796..84c0f1ef1c9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -31,6 +31,7 @@ + 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 00000000000..c477ff5637d --- /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/PortableXaml/AvaloniaDefaultTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs index 66e9a697e45..f8b99f2fdfe 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs @@ -32,6 +32,7 @@ public class AvaloniaDefaultTypeConverters { 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) }, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/MatrixTests.cs new file mode 100644 index 00000000000..4c1e361952a --- /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 From f05cb39a36bdd8226005a652c97db5c31838894c Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 15:00:18 +0300 Subject: [PATCH 03/10] Allow Geometry classes to be instantiated in XAML --- src/Avalonia.Visuals/Media/EllipseGeometry.cs | 51 ++++++-- src/Avalonia.Visuals/Media/LineGeometry.cs | 80 ++++++++++-- .../Media/PolylineGeometry.cs | 119 +++++++++++++++--- .../Media/RectangleGeometry.cs | 51 ++++++-- src/Avalonia.Visuals/Points.cs | 9 ++ 5 files changed, 256 insertions(+), 54 deletions(-) create mode 100644 src/Avalonia.Visuals/Points.cs diff --git a/src/Avalonia.Visuals/Media/EllipseGeometry.cs b/src/Avalonia.Visuals/Media/EllipseGeometry.cs index 414fd9ab596..591b55cf58f 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 @@ public EllipseGeometry(Rect rect) 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/LineGeometry.cs b/src/Avalonia.Visuals/Media/LineGeometry.cs index 2783d7fb262..323bfa5a7ea 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/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index 709ad7a9f58..c8d7ac163c9 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,122 @@ 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)); + + static PolylineGeometry() + { + PointsProperty.Changed.Subscribe(onNext: v => + { + (v.Sender as PolylineGeometry)?.OnPointsChanged(v.OldValue as Points, v.NewValue as Points); + }); + IsFilledProperty.Changed.AddClassHandler(x => a => x.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(); + } + + /// + /// Initializes a new instance of the class. + /// + public PolylineGeometry(IEnumerable points, bool isFilled) : this() + { + Points.AddRange(points); + IsFilled = isFilled; + + PrepareIfNeeded(); + } - using (IStreamGeometryContextImpl context = impl.Open()) + 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; + } + + private Points _points; + private bool _isDirty; + private IDisposable _pointsObserver; + /// 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 ef7deaa6f6d..1aa449d9e1a 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 @@ public RectangleGeometry(Rect rect) context.LineTo(rect.BottomLeft); context.EndFigure(true); } - - PlatformImpl = impl; - } - - /// - public override Geometry Clone() - { - return new RectangleGeometry(Bounds); } } } diff --git a/src/Avalonia.Visuals/Points.cs b/src/Avalonia.Visuals/Points.cs new file mode 100644 index 00000000000..867d3d4d246 --- /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 { } +} From d9b20fada4585fe0710a0290c448ed11358944d9 Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 15:51:06 +0300 Subject: [PATCH 04/10] Rect parse & type converter --- src/Avalonia.Visuals/Rect.cs | 26 +++++++++++++++++++ src/Avalonia.Visuals/RelativeRect.cs | 2 +- .../Avalonia.Markup.Xaml.csproj | 1 + .../Converters/RectTypeConverter.cs | 23 ++++++++++++++++ .../AvaloniaDefaultTypeConverters.cs | 1 + .../Media/RectTests.cs | 16 ++++++++++++ 6 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Converters/RectTypeConverter.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 0132c5e8a30..d562429fc7e 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -481,5 +481,31 @@ public override string ToString() _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 3ce3797c49c..a11f080e945 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.cs @@ -203,7 +203,7 @@ public static RelativeRect Parse(string s, CultureInfo culture) } 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 84c0f1ef1c9..08ea6b68779 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -32,6 +32,7 @@ + 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 00000000000..c9c6462f896 --- /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 f8b99f2fdfe..1cf5b6a58eb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs @@ -39,6 +39,7 @@ public class AvaloniaDefaultTypeConverters { 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/RectTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/RectTests.cs new file mode 100644 index 00000000000..12070bfed34 --- /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 From 42115b6ade1f572d6fa04de8f008e0f7b66a214a Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 15:52:37 +0300 Subject: [PATCH 05/10] Fix #1107 --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 1 + tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9c5ffe7151f..fad55ed531d 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -141,6 +141,7 @@ public void Parse(string s) 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/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index f0d41680c06..3b903b4436c 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -56,6 +56,7 @@ public void Parses_Close() } [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")] From b9aa086f034b1aeea93d7cab7c2f08442acab45e Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 16:18:09 +0300 Subject: [PATCH 06/10] Adding Drawing classes & render test --- samples/RenderTest/MainWindow.xaml | 1 + samples/RenderTest/Pages/DrawingPage.xaml | 132 ++++++++++++++++++ samples/RenderTest/Pages/DrawingPage.xaml.cs | 18 +++ samples/RenderTest/RenderTest.csproj | 8 ++ src/Avalonia.Controls/DrawingPresenter.cs | 58 ++++++++ src/Avalonia.Controls/Shapes/Shape.cs | 28 ++-- src/Avalonia.Visuals/Media/Drawing.cs | 9 ++ src/Avalonia.Visuals/Media/DrawingGroup.cs | 58 ++++++++ src/Avalonia.Visuals/Media/GeometryDrawing.cs | 43 ++++++ 9 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 samples/RenderTest/Pages/DrawingPage.xaml create mode 100644 samples/RenderTest/Pages/DrawingPage.xaml.cs create mode 100644 src/Avalonia.Controls/DrawingPresenter.cs create mode 100644 src/Avalonia.Visuals/Media/Drawing.cs create mode 100644 src/Avalonia.Visuals/Media/DrawingGroup.cs create mode 100644 src/Avalonia.Visuals/Media/GeometryDrawing.cs diff --git a/samples/RenderTest/MainWindow.xaml b/samples/RenderTest/MainWindow.xaml index 1d3001f1b16..9e9a600161e 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 00000000000..a1a75b19621 --- /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 00000000000..3bf9bd545d0 --- /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 ea5c0bcc587..b7e64f4daeb 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 00000000000..50a884d3bde --- /dev/null +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -0,0 +1,58 @@ +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Metadata; + +namespace Avalonia.Controls +{ + public class DrawingPresenter : Control + { + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + [Content] + public Drawing Drawing + { + get => GetValue(DrawingProperty); + set => SetValue(DrawingProperty, value); + } + + public static readonly StyledProperty StretchProperty = + AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); + + public Stretch Stretch + { + get => GetValue(StretchProperty); + set => SetValue(StretchProperty, value); + } + + static DrawingPresenter() + { + AffectsMeasure(DrawingProperty); + AffectsRender(DrawingProperty); + } + + 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)) + { + 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 427749263a4..c03f4dc5630 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -155,11 +155,21 @@ protected override Size MeasureOverride(Size availableSize) { // 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 @@ protected override Size MeasureOverride(Size availableSize) 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/Media/Drawing.cs b/src/Avalonia.Visuals/Media/Drawing.cs new file mode 100644 index 00000000000..a60c591edcd --- /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 00000000000..623a4bf6407 --- /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 + { + [Content] + public AvaloniaList Children { get; } = new AvaloniaList(); + + public static readonly StyledProperty OpacityProperty = + AvaloniaProperty.Register(nameof(Opacity), 1); + + public double Opacity + { + get => GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } + + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); + + public Transform Transform + { + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); + } + + 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/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs new file mode 100644 index 00000000000..e67e853a846 --- /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 From 7bbbb185ded1fac3541b78986a4e52658a74a84b Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 16:26:25 +0300 Subject: [PATCH 07/10] Typo --- samples/RenderTest/Pages/DrawingPage.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/RenderTest/Pages/DrawingPage.xaml b/samples/RenderTest/Pages/DrawingPage.xaml index a1a75b19621..81181e01fce 100644 --- a/samples/RenderTest/Pages/DrawingPage.xaml +++ b/samples/RenderTest/Pages/DrawingPage.xaml @@ -64,7 +64,7 @@ + Stretch="Fill" /> + Stretch="Uniform" /> + Stretch="UniformToFill" /> From c434cb215ad240b97cda43dac8388337f527e39d Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 19:11:45 +0300 Subject: [PATCH 08/10] PR comments --- src/Avalonia.Controls/DrawingPresenter.cs | 18 +++++++++--------- src/Avalonia.Visuals/Matrix.cs | 2 +- src/Avalonia.Visuals/Media/DrawingGroup.cs | 12 ++++++------ src/Avalonia.Visuals/Media/PolylineGeometry.cs | 18 +++++++----------- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs index 50a884d3bde..b366f2df142 100644 --- a/src/Avalonia.Controls/DrawingPresenter.cs +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -6,9 +6,18 @@ 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 { @@ -16,21 +25,12 @@ public Drawing Drawing set => SetValue(DrawingProperty, value); } - public static readonly StyledProperty StretchProperty = - AvaloniaProperty.Register(nameof(Stretch), Stretch.Uniform); - public Stretch Stretch { get => GetValue(StretchProperty); set => SetValue(StretchProperty, value); } - static DrawingPresenter() - { - AffectsMeasure(DrawingProperty); - AffectsRender(DrawingProperty); - } - private Matrix _transform = Matrix.Identity; protected override Size MeasureOverride(Size availableSize) diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index ee31ea91527..10549b967de 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.cs @@ -302,7 +302,7 @@ public Matrix Invert() /// /// The string. /// The current culture. - /// The . + /// The . public static Matrix Parse(string s, CultureInfo culture) { var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) diff --git a/src/Avalonia.Visuals/Media/DrawingGroup.cs b/src/Avalonia.Visuals/Media/DrawingGroup.cs index 623a4bf6407..744ff2af031 100644 --- a/src/Avalonia.Visuals/Media/DrawingGroup.cs +++ b/src/Avalonia.Visuals/Media/DrawingGroup.cs @@ -5,27 +5,27 @@ namespace Avalonia.Media { public class DrawingGroup : Drawing { - [Content] - public AvaloniaList Children { get; } = new AvaloniaList(); - 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 static readonly StyledProperty TransformProperty = - AvaloniaProperty.Register(nameof(Transform)); - 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)) diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index c8d7ac163c9..7c47e7d04d4 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -26,13 +26,15 @@ public class PolylineGeometry : Geometry public static readonly AvaloniaProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); + private Points _points; + private bool _isDirty; + private IDisposable _pointsObserver; + static PolylineGeometry() { - PointsProperty.Changed.Subscribe(onNext: v => - { - (v.Sender as PolylineGeometry)?.OnPointsChanged(v.OldValue as Points, v.NewValue as Points); - }); - IsFilledProperty.Changed.AddClassHandler(x => a => x.NotifyChanged()); + PointsProperty.Changed.AddClassHandler((s, e) => + s.OnPointsChanged(e.OldValue as Points, e.NewValue as Points)); + IsFilledProperty.Changed.AddClassHandler((s, _) => s.NotifyChanged()); } /// @@ -53,8 +55,6 @@ public PolylineGeometry(IEnumerable points, bool isFilled) : this() { Points.AddRange(points); IsFilled = isFilled; - - PrepareIfNeeded(); } public void PrepareIfNeeded() @@ -109,10 +109,6 @@ public override IGeometryImpl PlatformImpl protected set => base.PlatformImpl = value; } - private Points _points; - private bool _isDirty; - private IDisposable _pointsObserver; - /// public override Geometry Clone() { From 578c911d112e3113b0cef437ed3432c803778b47 Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Sun, 20 Aug 2017 19:23:03 +0300 Subject: [PATCH 09/10] Clipping --- src/Avalonia.Controls/DrawingPresenter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs index b366f2df142..af3665fabc3 100644 --- a/src/Avalonia.Controls/DrawingPresenter.cs +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -49,6 +49,7 @@ public override void Render(DrawingContext context) if (Drawing != null) { using (context.PushPreTransform(_transform)) + using (context.PushClip(Bounds)) { Drawing.Draw(context); } From 8ac3a181aac7b5131bc7ca9de02159e03b78c643 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Aug 2017 19:49:19 +0200 Subject: [PATCH 10/10] Allow for a null PlatformBrush in DrawingContextImpl.DrawImage Fixes #1119 --- src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 0b46ba1c479..69b582b009a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -119,7 +119,10 @@ public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRe using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_renderTarget.Factory, destRect.ToDirect2D())) { - d2dOpacityMask.PlatformBrush.Transform = Matrix.CreateTranslation(opacityMaskRect.Position).ToDirect2D(); + if (d2dOpacityMask.PlatformBrush != null) + { + d2dOpacityMask.PlatformBrush.Transform = Matrix.CreateTranslation(opacityMaskRect.Position).ToDirect2D(); + } _renderTarget.FillGeometry( geometry,