From d2e1eba876bff14206257a1d606052ccb1e7e79d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 Jan 2018 00:55:52 +0100 Subject: [PATCH] Refactored geometry. There were a number of problems with `Geometry` and its subclasses and platform implementations. Fix these, for more details see the PR that this commit is a part of. --- src/Avalonia.Visuals/Media/EllipseGeometry.cs | 33 +++-- src/Avalonia.Visuals/Media/Geometry.cs | 126 +++++++++++++++--- src/Avalonia.Visuals/Media/GeometryDrawing.cs | 3 +- src/Avalonia.Visuals/Media/LineGeometry.cs | 66 ++++----- src/Avalonia.Visuals/Media/PathGeometry.cs | 68 +++------- .../Media/PolylineGeometry.cs | 73 ++++------ .../Media/RectangleGeometry.cs | 24 ++-- src/Avalonia.Visuals/Media/StreamGeometry.cs | 20 ++- .../Platform/IGeometryImpl.cs | 18 +-- .../Platform/ITransformedGeometryImpl.cs | 24 ++++ .../Rendering/SceneGraph/GeometryNode.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- src/Skia/Avalonia.Skia/GeometryImpl.cs | 19 +++ src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 58 +++----- .../Avalonia.Skia/TransformedGeometryImpl.cs | 63 +++++++++ .../Media/DrawingContextImpl.cs | 2 +- .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 14 +- .../Media/TransformedGeometryImpl.cs | 10 +- .../Avalonia.Direct2D1/PrimitiveExtensions.cs | 15 ++- .../Media/GeometryTests.cs | 3 +- .../MockStreamGeometryImpl.cs | 12 +- .../Media/GeometryTests.cs | 126 ++++++++++++++++++ .../Media/RectangleGeometryTests.cs | 2 +- .../VisualTree/MockRenderInterface.cs | 19 +-- 24 files changed, 543 insertions(+), 259 deletions(-) create mode 100644 src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs create mode 100644 src/Skia/Avalonia.Skia/GeometryImpl.cs create mode 100644 src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs diff --git a/src/Avalonia.Visuals/Media/EllipseGeometry.cs b/src/Avalonia.Visuals/Media/EllipseGeometry.cs index 591b55cf58..e233eab496 100644 --- a/src/Avalonia.Visuals/Media/EllipseGeometry.cs +++ b/src/Avalonia.Visuals/Media/EllipseGeometry.cs @@ -17,15 +17,9 @@ namespace Avalonia.Media 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); + AffectsGeometry(RectProperty); } /// @@ -33,8 +27,6 @@ namespace Avalonia.Media /// public EllipseGeometry() { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.CreateStreamGeometry(); } /// @@ -46,17 +38,30 @@ namespace Avalonia.Media Rect = rect; } + /// + /// Gets or sets a rect that defines the bounds of the ellipse. + /// + public Rect Rect + { + get => GetValue(RectProperty); + set => SetValue(RectProperty, value); + } + /// public override Geometry Clone() { return new EllipseGeometry(Rect); } - private void RectChanged(AvaloniaPropertyChangedEventArgs e) + /// + protected override IGeometryImpl CreateDefiningGeometry() { - var rect = (Rect)e.NewValue; - using (var ctx = ((IStreamGeometryImpl)PlatformImpl).Open()) + var factory = AvaloniaLocator.Current.GetService(); + var geometry = factory.CreateStreamGeometry(); + + using (var ctx = geometry.Open()) { + var rect = Rect; double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3; var center = rect.Center; var radius = new Vector(rect.Width / 2, rect.Height / 2); @@ -80,6 +85,10 @@ namespace Avalonia.Media ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0)); ctx.EndFigure(true); } + + return geometry; } + + private void RectChanged(AvaloniaPropertyChangedEventArgs e) => InvalidateGeometry(); } } diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index d27626bcc1..8201736238 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -17,26 +17,47 @@ namespace Avalonia.Media public static readonly StyledProperty TransformProperty = AvaloniaProperty.Register(nameof(Transform)); - /// - /// Initializes static members of the class. - /// + private bool _isDirty = true; + private IGeometryImpl _platformImpl; + static Geometry() { TransformProperty.Changed.AddClassHandler(x => x.TransformChanged); } + /// + /// Raised when the geometry changes. + /// + public event EventHandler Changed; + /// /// Gets the geometry's bounding rectangle. /// - public Rect Bounds => PlatformImpl.Bounds; + public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty; /// /// Gets the platform-specific implementation of the geometry. /// - public virtual IGeometryImpl PlatformImpl + public IGeometryImpl PlatformImpl { - get; - protected set; + get + { + if (_isDirty) + { + var geometry = CreateDefiningGeometry(); + var transform = Transform; + + if (geometry != null && transform != null && transform.Value != Matrix.Identity) + { + geometry = geometry.WithTransform(transform.Value); + } + + _platformImpl = geometry; + _isDirty = false; + } + + return _platformImpl; + } } /// @@ -55,14 +76,11 @@ namespace Avalonia.Media public abstract Geometry Clone(); /// - /// Gets the geometry's bounding rectangle with the specified stroke thickness. + /// Gets the geometry's bounding rectangle with the specified pen. /// - /// The stroke thickness. + /// The stroke thickness. /// The bounding rectangle. - public Rect GetRenderBounds(double strokeThickness) - { - return PlatformImpl.GetRenderBounds(strokeThickness); - } + public Rect GetRenderBounds(Pen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty; /// /// Indicates whether the geometry's fill contains the specified point. @@ -71,7 +89,7 @@ namespace Avalonia.Media /// true if the geometry contains the point; otherwise, false. public bool FillContains(Point point) { - return PlatformImpl.FillContains(point); + return PlatformImpl?.FillContains(point) ?? false; } /// @@ -82,13 +100,87 @@ namespace Avalonia.Media /// true if the geometry contains the point; otherwise, false. public bool StrokeContains(Pen pen, Point point) { - return PlatformImpl.StrokeContains(pen, point); + return PlatformImpl?.StrokeContains(pen, point) ?? false; + } + + /// + /// Marks a property as affecting the geometry's . + /// + /// The properties. + /// + /// After a call to this method in a control's static constructor, any change to the + /// property will cause to be called on the element. + /// + protected static void AffectsGeometry(params AvaloniaProperty[] properties) + { + foreach (var property in properties) + { + property.Changed.Subscribe(AffectsGeometryInvalidate); + } + } + + /// + /// Creates the platform implementation of the geometry, without the transform applied. + /// + /// + protected abstract IGeometryImpl CreateDefiningGeometry(); + + /// + /// Invalidates the platform implementation of the geometry. + /// + protected void InvalidateGeometry() + { + _isDirty = true; + _platformImpl?.Dispose(); + _platformImpl = null; + Changed?.Invoke(this, EventArgs.Empty); } private void TransformChanged(AvaloniaPropertyChangedEventArgs e) { - var transform = (Transform)e.NewValue; - PlatformImpl = PlatformImpl.WithTransform(transform.Value); + var oldValue = (Transform)e.OldValue; + var newValue = (Transform)e.NewValue; + + if (oldValue != null) + { + oldValue.Changed -= TransformChanged; + } + + if (newValue != null) + { + newValue.Changed += TransformChanged; + } + + TransformChanged(newValue, EventArgs.Empty); + } + + private void TransformChanged(object sender, EventArgs e) + { + var transform = ((Transform)sender)?.Value; + + if (_platformImpl is ITransformedGeometryImpl t) + { + if (transform == null || transform == Matrix.Identity) + { + _platformImpl = t.SourceGeometry; + } + else if (transform != t.Transform) + { + _platformImpl = t.SourceGeometry.WithTransform(transform.Value); + } + } + else if (_platformImpl != null && transform != null && transform != Matrix.Identity) + { + _platformImpl = PlatformImpl.WithTransform(transform.Value); + } + + Changed?.Invoke(this, EventArgs.Empty); + } + + private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e) + { + var control = e.Sender as Geometry; + control?.InvalidateGeometry(); } } } diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs index e67e853a84..a26a5341c8 100644 --- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs +++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs @@ -37,7 +37,8 @@ public override Rect GetBounds() { // adding the Pen's stroke thickness here could yield wrong results due to transforms - return Geometry?.GetRenderBounds(0) ?? new Rect(); + var pen = new Pen(Brushes.Black, 0); + return Geometry?.GetRenderBounds(pen) ?? 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 0952d9644f..f7ba4ccb0e 100644 --- a/src/Avalonia.Visuals/Media/LineGeometry.cs +++ b/src/Avalonia.Visuals/Media/LineGeometry.cs @@ -16,29 +16,15 @@ namespace Avalonia.Media 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 = true; - - public Point EndPoint - { - get => GetValue(EndPointProperty); - set => SetValue(EndPointProperty, value); - } static LineGeometry() { - StartPointProperty.Changed.AddClassHandler(x => x.PointsChanged); - EndPointProperty.Changed.AddClassHandler(x => x.PointsChanged); + AffectsGeometry(StartPointProperty, EndPointProperty); } /// @@ -46,8 +32,6 @@ namespace Avalonia.Media /// public LineGeometry() { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.CreateStreamGeometry(); } /// @@ -61,38 +45,44 @@ namespace Avalonia.Media EndPoint = endPoint; } - public override IGeometryImpl PlatformImpl + /// + /// Gets or sets the start point of the line. + /// + public Point StartPoint { - get - { - PrepareIfNeeded(); - return base.PlatformImpl; - } - protected set => base.PlatformImpl = value; + get => GetValue(StartPointProperty); + set => SetValue(StartPointProperty, value); } - public void PrepareIfNeeded() + /// + /// Gets or sets the end point of the line. + /// + public Point EndPoint { - if (_isDirty) - { - _isDirty = false; - - using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) - { - context.BeginFigure(StartPoint, false); - context.LineTo(EndPoint); - context.EndFigure(false); - } - } + get => GetValue(EndPointProperty); + set => SetValue(EndPointProperty, value); } /// public override Geometry Clone() { - PrepareIfNeeded(); return new LineGeometry(StartPoint, EndPoint); } - private void PointsChanged(AvaloniaPropertyChangedEventArgs e) => _isDirty = true; + /// + protected override IGeometryImpl CreateDefiningGeometry() + { + var factory = AvaloniaLocator.Current.GetService(); + var geometry = factory.CreateStreamGeometry(); + + using (var context = geometry.Open()) + { + context.BeginFigure(StartPoint, false); + context.LineTo(EndPoint); + context.EndFigure(false); + } + + return geometry; + } } } diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index df3dd47c8a..3e27d8a461 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -1,10 +1,10 @@ // 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.Collections; using Avalonia.Metadata; using Avalonia.Platform; -using System; namespace Avalonia.Media { @@ -22,12 +22,14 @@ namespace Avalonia.Media public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); + private PathFigures _figures; + private IDisposable _figuresObserver = null; + private IDisposable _figuresPropertiesObserver = null; + static PathGeometry() { - FiguresProperty.Changed.Subscribe(onNext: v => - { - (v.Sender as PathGeometry)?.OnFiguresChanged(v.OldValue as PathFigures, v.NewValue as PathFigures); - }); + FiguresProperty.Changed.AddClassHandler((s, e) => + s.OnFiguresChanged(e.NewValue as PathFigures)); } /// @@ -63,61 +65,33 @@ namespace Avalonia.Media set { SetValue(FillRuleProperty, value); } } - public override IGeometryImpl PlatformImpl + protected override IGeometryImpl CreateDefiningGeometry() { - get - { - PrepareIfNeeded(); - return base.PlatformImpl; - } + var factory = AvaloniaLocator.Current.GetService(); + var geometry = factory.CreateStreamGeometry(); - protected set + using (var ctx = new StreamGeometryContext(geometry.Open())) { - base.PlatformImpl = value; - } - } - - public override Geometry Clone() - { - PrepareIfNeeded(); - - return base.Clone(); - } - - public void PrepareIfNeeded() - { - if (_isDirty) - { - _isDirty = false; - - using (var ctx = Open()) + ctx.SetFillRule(FillRule); + foreach (var f in Figures) { - ctx.SetFillRule(FillRule); - foreach (var f in Figures) - { - f.ApplyTo(ctx); - } + f.ApplyTo(ctx); } } - } - internal void NotifyChanged() - { - _isDirty = true; + return geometry; } - private PathFigures _figures; - private IDisposable _figuresObserver = null; - private IDisposable _figuresPropertiesObserver = null; - private bool _isDirty = true; - - private void OnFiguresChanged(PathFigures oldValue, PathFigures newValue) + private void OnFiguresChanged(PathFigures figures) { _figuresObserver?.Dispose(); _figuresPropertiesObserver?.Dispose(); - _figuresObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged()); - _figuresPropertiesObserver = newValue?.TrackItemPropertyChanged(t => NotifyChanged()); + _figuresObserver = figures?.ForEachItem( + _ => InvalidateGeometry(), + _ => InvalidateGeometry(), + () => InvalidateGeometry()); + _figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry()); } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/PolylineGeometry.cs b/src/Avalonia.Visuals/Media/PolylineGeometry.cs index b23bb88729..06dbcccf3e 100644 --- a/src/Avalonia.Visuals/Media/PolylineGeometry.cs +++ b/src/Avalonia.Visuals/Media/PolylineGeometry.cs @@ -27,14 +27,12 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(IsFilled)); private Points _points; - private bool _isDirty = true; 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()); + AffectsGeometry(IsFilledProperty); + PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as Points)); } /// @@ -42,9 +40,6 @@ namespace Avalonia.Media /// public PolylineGeometry() { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.CreateStreamGeometry(); - Points = new Points(); } @@ -57,29 +52,6 @@ namespace Avalonia.Media IsFilled = isFilled; } - public void PrepareIfNeeded() - { - if (_isDirty) - { - _isDirty = false; - - using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) - { - var points = Points; - var isFilled = IsFilled; - if (points.Count > 0) - { - context.BeginFigure(points[0], isFilled); - for (int i = 1; i < points.Count; i++) - { - context.LineTo(points[i]); - } - context.EndFigure(isFilled); - } - } - } - } - /// /// Gets or sets the figures. /// @@ -99,33 +71,42 @@ namespace Avalonia.Media set => SetValue(IsFilledProperty, value); } - public override IGeometryImpl PlatformImpl - { - get - { - PrepareIfNeeded(); - return base.PlatformImpl; - } - protected set => base.PlatformImpl = value; - } - /// public override Geometry Clone() { - PrepareIfNeeded(); return new PolylineGeometry(Points, IsFilled); } - private void OnPointsChanged(Points oldValue, Points newValue) + protected override IGeometryImpl CreateDefiningGeometry() { - _pointsObserver?.Dispose(); + var factory = AvaloniaLocator.Current.GetService(); + var geometry = factory.CreateStreamGeometry(); + + using (var context = geometry.Open()) + { + var points = Points; + var isFilled = IsFilled; + if (points.Count > 0) + { + context.BeginFigure(points[0], isFilled); + for (int i = 1; i < points.Count; i++) + { + context.LineTo(points[i]); + } + context.EndFigure(isFilled); + } + } - _pointsObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged()); + return geometry; } - internal void NotifyChanged() + private void OnPointsChanged(Points newValue) { - _isDirty = true; + _pointsObserver?.Dispose(); + _pointsObserver = newValue?.ForEachItem( + _ => InvalidateGeometry(), + _ => InvalidateGeometry(), + InvalidateGeometry); } } } diff --git a/src/Avalonia.Visuals/Media/RectangleGeometry.cs b/src/Avalonia.Visuals/Media/RectangleGeometry.cs index 1aa449d9e1..fd1e8ac859 100644 --- a/src/Avalonia.Visuals/Media/RectangleGeometry.cs +++ b/src/Avalonia.Visuals/Media/RectangleGeometry.cs @@ -16,6 +16,8 @@ namespace Avalonia.Media public static readonly StyledProperty RectProperty = AvaloniaProperty.Register(nameof(Rect)); + bool _isDirty = true; + public Rect Rect { get => GetValue(RectProperty); @@ -24,7 +26,7 @@ namespace Avalonia.Media static RectangleGeometry() { - RectProperty.Changed.AddClassHandler(x => x.RectChanged); + AffectsGeometry(RectProperty); } /// @@ -32,36 +34,36 @@ namespace Avalonia.Media /// public RectangleGeometry() { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.CreateStreamGeometry(); } /// /// Initializes a new instance of the class. /// /// The rectangle bounds. - public RectangleGeometry(Rect rect) : this() + public RectangleGeometry(Rect rect) { Rect = rect; } /// - public override Geometry Clone() - { - return new RectangleGeometry(Rect); - } + public override Geometry Clone() => new RectangleGeometry(Rect); - private void RectChanged(AvaloniaPropertyChangedEventArgs e) + protected override IGeometryImpl CreateDefiningGeometry() { - var rect = (Rect)e.NewValue; - using (var context = ((IStreamGeometryImpl)PlatformImpl).Open()) + var factory = AvaloniaLocator.Current.GetService(); + var geometry = factory.CreateStreamGeometry(); + + using (var context = geometry.Open()) { + var rect = Rect; context.BeginFigure(rect.TopLeft, true); context.LineTo(rect.TopRight); context.LineTo(rect.BottomRight); context.LineTo(rect.BottomLeft); context.EndFigure(true); } + + return geometry; } } } diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index a1f62b172d..9c29c62bad 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -10,22 +10,22 @@ namespace Avalonia.Media /// public class StreamGeometry : Geometry { + IStreamGeometryImpl _impl; + /// /// Initializes a new instance of the class. /// public StreamGeometry() { - IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService(); - PlatformImpl = factory.CreateStreamGeometry(); } /// /// Initializes a new instance of the class. /// /// The platform-specific implementation. - private StreamGeometry(IGeometryImpl impl) + private StreamGeometry(IStreamGeometryImpl impl) { - PlatformImpl = impl; + _impl = impl; } /// @@ -61,5 +61,17 @@ namespace Avalonia.Media { return new StreamGeometryContext(((IStreamGeometryImpl)PlatformImpl).Open()); } + + /// + protected override IGeometryImpl CreateDefiningGeometry() + { + if (_impl == null) + { + var factory = AvaloniaLocator.Current.GetService(); + _impl = factory.CreateStreamGeometry(); + } + + return _impl; + } } } diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index 132e00e56b..fb813a5981 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -1,14 +1,15 @@ // 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 { /// - /// Defines the platform-specific interface for . + /// Defines the platform-specific interface for a . /// - public interface IGeometryImpl + public interface IGeometryImpl : IDisposable { /// /// Gets the geometry's bounding rectangle. @@ -16,16 +17,11 @@ namespace Avalonia.Platform Rect Bounds { get; } /// - /// Gets the transform to applied to the geometry. + /// Gets the geometry's bounding rectangle with the specified pen. /// - Matrix Transform { get; } - - /// - /// Gets the geometry's bounding rectangle with the specified stroke thickness. - /// - /// The stroke thickness. + /// The pen to use. May be null. /// The bounding rectangle. - Rect GetRenderBounds(double strokeThickness); + Rect GetRenderBounds(Pen pen); /// /// Indicates whether the geometry's fill contains the specified point. @@ -54,6 +50,6 @@ namespace Avalonia.Platform /// /// The transform. /// The cloned geometry. - IGeometryImpl WithTransform(Matrix transform); + ITransformedGeometryImpl WithTransform(Matrix transform); } } diff --git a/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs b/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs new file mode 100644 index 0000000000..ca68005906 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Platform +{ + /// + /// Represents a geometry with a transform applied. + /// + /// + /// An transforms a geometry without transforming its + /// stroke thickness. + /// + public interface ITransformedGeometryImpl : IGeometryImpl + { + /// + /// Gets the source geometry that the is applied to. + /// + IGeometryImpl SourceGeometry { get; } + + /// + /// Gets the applied transform. + /// + Matrix Transform { get; } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 6310122183..7b79ebab4f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph Pen pen, IGeometryImpl geometry, IDictionary childScenes = null) - : base(geometry.GetRenderBounds(pen?.Thickness ?? 0), transform, null) + : base(geometry.GetRenderBounds(pen), transform, null) { Transform = transform; Brush = brush?.ToImmutable(); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index dd3ced1d89..4defec4c07 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -68,7 +68,7 @@ namespace Avalonia.Skia public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = (StreamGeometryImpl)geometry; + var impl = (GeometryImpl)geometry; var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs new file mode 100644 index 0000000000..4ecd687777 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + abstract class GeometryImpl : IGeometryImpl + { + public abstract Rect Bounds { get; } + public abstract SKPath EffectivePath { get; } + public abstract void Dispose(); + public abstract bool FillContains(Point point); + public abstract Rect GetRenderBounds(Pen pen); + public abstract IGeometryImpl Intersect(IGeometryImpl geometry); + public abstract bool StrokeContains(Pen pen, Point point); + public abstract ITransformedGeometryImpl WithTransform(Matrix transform); + } +} diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 9a0a1dc434..e8ab5fc6da 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,46 +1,35 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.RenderHelpers; using SkiaSharp; namespace Avalonia.Skia { - class StreamGeometryImpl : IStreamGeometryImpl + class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { + Rect _bounds; SKPath _path; - private Matrix _transform = Matrix.Identity; + public override SKPath EffectivePath => _path; - public SKPath EffectivePath => _path; - - public Rect GetRenderBounds(double strokeThickness) + public override Rect GetRenderBounds(Pen pen) { - // TODO: Calculate properly. - return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); + return GetRenderBounds(pen?.Thickness ?? 0); } - public Rect Bounds { get; private set; } - - public Matrix Transform - { - get { return _transform; } - } + public override Rect Bounds => _bounds; public IStreamGeometryImpl Clone() { return new StreamGeometryImpl { _path = _path?.Clone(), - _transform = Transform, - Bounds = Bounds + _bounds = Bounds }; } + public override void Dispose() => _path.Dispose(); + public IStreamGeometryContextImpl Open() { _path = new SKPath(); @@ -49,41 +38,34 @@ namespace Avalonia.Skia return new StreamContext(this); } - public bool FillContains(Point point) + public override bool FillContains(Point point) { // TODO: Not supported by SkiaSharp yet, so use expanded Rect // return EffectivePath.Contains(point.X, point.Y); return GetRenderBounds(0).Contains(point); } - public bool StrokeContains(Pen pen, Point point) + public override bool StrokeContains(Pen pen, Point point) { // TODO: Not supported by SkiaSharp yet, so use expanded Rect // return EffectivePath.Contains(point.X, point.Y); return GetRenderBounds(0).Contains(point); } - public IGeometryImpl Intersect(IGeometryImpl geometry) + public override IGeometryImpl Intersect(IGeometryImpl geometry) { throw new NotImplementedException(); } - public IGeometryImpl WithTransform(Matrix transform) + public override ITransformedGeometryImpl WithTransform(Matrix transform) { - var result = (StreamGeometryImpl)Clone(); - - if (result.Transform != Matrix.Identity) - { - result._path.Transform(result.Transform.Invert().ToSKMatrix()); - } - - if (transform != Matrix.Identity) - { - result._path.Transform(transform.ToSKMatrix()); - } + return new TransformedGeometryImpl(this, transform); + } - result._transform = transform; - return result; + private Rect GetRenderBounds(double strokeThickness) + { + // TODO: Calculate properly. + return Bounds.Inflate(strokeThickness); } class StreamContext : IStreamGeometryContextImpl @@ -102,7 +84,7 @@ namespace Avalonia.Skia { SKRect rc; _path.GetBounds(out rc); - _geometryImpl.Bounds = rc.ToAvaloniaRect(); + _geometryImpl._bounds = rc.ToAvaloniaRect(); } public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs new file mode 100644 index 0000000000..b89a7d2f2e --- /dev/null +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using SkiaSharp; + +namespace Avalonia.Skia +{ + class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl + { + public TransformedGeometryImpl(GeometryImpl source, Matrix transform) + { + SourceGeometry = source; + Transform = transform; + EffectivePath = source.EffectivePath.Clone(); + EffectivePath.Transform(transform.ToSKMatrix()); + } + + public override SKPath EffectivePath { get; } + + public IGeometryImpl SourceGeometry { get; } + + public Matrix Transform { get; } + + public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform); + + public override void Dispose() + { + } + + public override bool FillContains(Point point) + { + // TODO: Not supported by SkiaSharp yet, so use expanded Rect + return GetRenderBounds(0).Contains(point); + } + + public override Rect GetRenderBounds(Pen pen) + { + return GetRenderBounds(pen.Thickness); + } + + public override IGeometryImpl Intersect(IGeometryImpl geometry) + { + throw new NotImplementedException(); + } + + public override bool StrokeContains(Pen pen, Point point) + { + // TODO: Not supported by SkiaSharp yet, so use expanded Rect + return GetRenderBounds(0).Contains(point); + } + + public override ITransformedGeometryImpl WithTransform(Matrix transform) + { + return new TransformedGeometryImpl(this, transform); + } + + public Rect GetRenderBounds(double strokeThickness) + { + // TODO: Calculate properly. + return Bounds.Inflate(strokeThickness); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index b1bfdcbfeb..04303130e5 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -188,7 +188,7 @@ namespace Avalonia.Direct2D1.Media if (pen != null) { - using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen.Thickness).Size)) + using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_renderTarget)) { if (d2dBrush.PlatformBrush != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index 8bb901a1e4..5de0218cce 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -23,13 +23,13 @@ namespace Avalonia.Direct2D1.Media /// public Geometry Geometry { get; } - /// - public virtual Matrix Transform => Matrix.Identity; + public void Dispose() => Geometry.Dispose(); /// - public Rect GetRenderBounds(double strokeThickness) + public Rect GetRenderBounds(Avalonia.Media.Pen pen) { - return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia(); + var factory = AvaloniaLocator.Current.GetService(); + return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia(); } /// @@ -56,15 +56,15 @@ namespace Avalonia.Direct2D1.Media return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); } - /// - public IGeometryImpl WithTransform(Matrix transform) + public ITransformedGeometryImpl WithTransform(Matrix transform) { var factory = AvaloniaLocator.Current.GetService(); return new TransformedGeometryImpl( new TransformedGeometry( factory, GetSourceGeometry(), - transform.ToDirect2D())); + transform.ToDirect2D()), + this); } protected virtual Geometry GetSourceGeometry() => Geometry; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs index 4043e180dc..e0e9e340bb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs @@ -1,23 +1,27 @@ // 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.Platform; using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media { - public class TransformedGeometryImpl : GeometryImpl + public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl { /// /// Initializes a new instance of the class. /// /// An existing Direct2D . - public TransformedGeometryImpl(TransformedGeometry geometry) + public TransformedGeometryImpl(TransformedGeometry geometry, GeometryImpl source) : base(geometry) { + SourceGeometry = source; } + public IGeometryImpl SourceGeometry { get; } + /// - public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia(); + public Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia(); protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry; } diff --git a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs index 118b6deb97..c76a5b5da5 100644 --- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs +++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs @@ -105,7 +105,18 @@ namespace Avalonia.Direct2D1 /// The pen to convert. /// The render target. /// The Direct2D brush. - public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget target) + public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget) + { + return pen.ToDirect2DStrokeStyle(renderTarget.Factory); + } + + /// + /// Converts a pen to a Direct2D stroke style. + /// + /// The pen to convert. + /// The render target. + /// The Direct2D brush. + public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory) { var properties = new StrokeStyleProperties { @@ -123,7 +134,7 @@ namespace Avalonia.Direct2D1 properties.DashOffset = (float)pen.DashStyle.Offset; dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray(); } - return new StrokeStyle(target.Factory, properties, dashes); + return new StrokeStyle(factory, properties, dashes); } /// diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs index 4b8933b0f8..963c75078b 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs @@ -31,8 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media Direct2D1Platform.Initialize(); var target = StreamGeometry.Parse("M 0 2 L 4 6 L 0 10 Z"); + var pen = new Pen(Brushes.Black, 2); - Assert.Equal(new Rect(-1, -0.414, 6.414, 12.828), target.GetRenderBounds(2), Compare); + Assert.Equal(new Rect(-1, -0.414, 6.414, 12.828), target.GetRenderBounds(pen), Compare); } } } diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs index 4ef864d843..63da9ed3f0 100644 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs @@ -5,7 +5,7 @@ using Avalonia.Platform; namespace Avalonia.UnitTests { - public class MockStreamGeometryImpl : IStreamGeometryImpl + public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl { private MockStreamGeometryContext _context; @@ -27,6 +27,8 @@ namespace Avalonia.UnitTests _context = context; } + public IGeometryImpl SourceGeometry { get; } + public Rect Bounds => _context.CalculateBounds(); public Matrix Transform { get; } @@ -36,6 +38,10 @@ namespace Avalonia.UnitTests return this; } + public void Dispose() + { + } + public bool FillContains(Point point) { return _context.FillContains(point); @@ -46,7 +52,7 @@ namespace Avalonia.UnitTests return false; } - public Rect GetRenderBounds(double strokeThickness) => Bounds; + public Rect GetRenderBounds(Pen pen) => Bounds; public IGeometryImpl Intersect(IGeometryImpl geometry) { @@ -58,7 +64,7 @@ namespace Avalonia.UnitTests return _context; } - public IGeometryImpl WithTransform(Matrix transform) + public ITransformedGeometryImpl WithTransform(Matrix transform) { return new MockStreamGeometryImpl(transform, _context); } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs new file mode 100644 index 0000000000..3f7e353749 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs @@ -0,0 +1,126 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using Moq; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class GeometryTests + { + [Fact] + public void Changing_AffectsGeometry_Property_Causes_PlatformImpl_To_Be_Updated() + { + var target = new TestGeometry(); + var platformImpl = target.PlatformImpl; + + target.Foo = true; + + Assert.NotSame(platformImpl, target.PlatformImpl); + } + + [Fact] + public void Changing_AffectsGeometry_Property_Causes_Changed_To_Be_Raised() + { + var target = new TestGeometry(); + var raised = false; + + target.Changed += (s, e) => raised = true; + target.Foo = true; + + Assert.True(raised); + } + + [Fact] + public void Old_PlatformImpl_Is_Disposed_When_Updated() + { + var target = new TestGeometry(); + var platformImpl = target.PlatformImpl; + + target.Foo = true; + + Mock.Get(platformImpl).Verify(x => x.Dispose()); + } + + [Fact] + public void Setting_Transform_Causes_Changed_To_Be_Raised() + { + var target = new TestGeometry(); + var raised = false; + + target.Changed += (s, e) => raised = true; + target.Transform = new RotateTransform(45); + + Assert.True(raised); + } + + [Fact] + public void Changing_Transform_Causes_Changed_To_Be_Raised() + { + var transform = new RotateTransform(45); + var target = new TestGeometry { Transform = transform }; + var raised = false; + + target.Changed += (s, e) => raised = true; + transform.Angle = 90; + + Assert.True(raised); + } + + [Fact] + public void Removing_Transform_Causes_Changed_To_Be_Raised() + { + var transform = new RotateTransform(45); + var target = new TestGeometry { Transform = transform }; + var raised = false; + + target.Changed += (s, e) => raised = true; + target.Transform = null; + + Assert.True(raised); + } + + [Fact] + public void Transform_Produces_Transformed_PlatformImpl() + { + var target = new TestGeometry(); + var rotate = new RotateTransform(45); + + Assert.False(target.PlatformImpl is ITransformedGeometryImpl); + target.Transform = rotate; + Assert.True(target.PlatformImpl is ITransformedGeometryImpl); + rotate.Angle = 0; + Assert.False(target.PlatformImpl is ITransformedGeometryImpl); + } + + private class TestGeometry : Geometry + { + public static readonly AvaloniaProperty FooProperty = + AvaloniaProperty.Register(nameof(Foo)); + + static TestGeometry() + { + AffectsGeometry(FooProperty); + } + + public bool Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + + public override Geometry Clone() + { + throw new NotImplementedException(); + } + + protected override IGeometryImpl CreateDefiningGeometry() + { + return Mock.Of( + x => x.WithTransform(It.IsAny()) == + Mock.Of(y => + y.SourceGeometry == x)); + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs index 9f94e7fab4..5af1bb572e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Visuals.UnitTests.Media private TestServices GetServices() { var context = Mock.Of(); - var transformedGeometry = new Mock(); + var transformedGeometry = new Mock(); var streamGeometry = Mock.Of(x => x.Open() == context && x.WithTransform(It.IsAny()) == transformedGeometry.Object); diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 5fcf1cf1f2..54bb5d72d0 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -65,22 +65,13 @@ namespace Avalonia.Visuals.UnitTests.VisualTree } } - public Matrix Transform + public IStreamGeometryImpl Clone() { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } + return this; } - public IStreamGeometryImpl Clone() + public void Dispose() { - return this; } public bool FillContains(Point point) @@ -88,7 +79,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return _impl.FillContains(point); } - public Rect GetRenderBounds(double strokeThickness) + public Rect GetRenderBounds(Pen pen) { throw new NotImplementedException(); } @@ -108,7 +99,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } - public IGeometryImpl WithTransform(Matrix transform) + public ITransformedGeometryImpl WithTransform(Matrix transform) { throw new NotImplementedException(); }