From b0317f46a5bf2aa524b028b0aac011392a0dd9d0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 28 Nov 2021 20:38:49 +0100 Subject: [PATCH 1/4] Add support for drawing ellipses directly via DrawingContext --- .../HeadlessPlatformRenderInterface.cs | 4 + src/Avalonia.Visuals/Media/DrawingContext.cs | 28 ++++- .../Platform/IDrawingContextImpl.cs | 12 ++ .../SceneGraph/DeferredDrawingContextImpl.cs | 14 +++ .../Rendering/SceneGraph/EllipseNode.cs | 118 ++++++++++++++++++ .../Rendering/SceneGraph/RectangleNode.cs | 3 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 28 +++++ .../Media/DrawingContextImpl.cs | 39 ++++++ .../NullDrawingContextImpl.cs | 4 + 9 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 63cbfb2dbe..48d0ef9da9 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -447,6 +447,10 @@ namespace Avalonia.Headless } + public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + { + } + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 4e3dc8699c..8e8b116a04 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; @@ -190,6 +189,33 @@ namespace Avalonia.Media DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); } + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The location of the center of the ellipse. + /// The horizontal radius of the ellipse. + /// The vertical radius of the ellipse. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IBrush brush, IPen pen, Point center, double radiusX, double radiusY) + { + if (brush == null && !PenIsVisible(pen)) + { + return; + } + + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + + PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + /// /// Draws a custom drawing operation /// diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 39d4066e55..ac2c5c9f08 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -71,6 +71,18 @@ namespace Avalonia.Platform void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default); + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The ellipse bounds. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + void DrawEllipse(IBrush brush, IPen pen, Rect rect); + /// /// Draws text. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index f4039dc0bc..da1a00504a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -179,6 +179,20 @@ namespace Avalonia.Rendering.SceneGraph } } + public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) + { + Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + } + else + { + ++_drawOperationindex; + } + } + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs new file mode 100644 index 0000000000..c817303d51 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents an ellipse draw. + /// + internal class EllipseNode : BrushDrawOperation + { + public EllipseNode( + Matrix transform, + IBrush brush, + IPen pen, + Rect rect, + IDictionary childScenes = null) + : base(rect.Inflate(pen?.Thickness ?? 0), transform) + { + Transform = transform; + Brush = brush?.ToImmutable(); + Pen = pen?.ToImmutable(); + Rect = rect; + ChildScenes = childScenes; + } + + /// + /// Gets the fill brush. + /// + public IBrush Brush { get; } + + /// + /// Gets the stroke pen. + /// + public ImmutablePen Pen { get; } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + /// + /// Gets the rect of the ellipse to draw. + /// + public Rect Rect { get; } + + public override IDictionary ChildScenes { get; } + + public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect) + { + return transform == Transform && + Equals(brush, Brush) && + Equals(Pen, pen) && + rect.Equals(Rect); + } + + public override void Render(IDrawingContextImpl context) + { + context.DrawEllipse(Brush, Pen, Rect); + } + + public override bool HitTest(Point p) + { + if (!Transform.TryInvert(out Matrix inverted)) + { + return false; + } + + p *= inverted; + + var center = Rect.Center; + + var strokeThickness = Pen?.Thickness ?? 0; + + var rx = Rect.Width / 2 + strokeThickness / 2; + var ry = Rect.Height / 2 + strokeThickness / 2; + + var dx = p.X - center.X; + var dy = p.Y - center.Y; + + if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) + { + return false; + } + + if (Brush != null) + { + return Contains(rx, ry); + } + else if (strokeThickness > 0) + { + bool inStroke = Contains(rx, ry); + + rx = Rect.Width / 2 - strokeThickness / 2; + ry = Rect.Height / 2 - strokeThickness / 2; + + bool inInner = Contains(rx, ry); + + return inStroke && !inInner; + } + + bool Contains(double radiusX, double radiusY) + { + var rx2 = radiusX * radiusX; + var ry2 = radiusY * radiusY; + + var distance = ry2 * dx * dx + rx2 * dy * dy; + + return distance < rx2 * ry2; + } + + return false; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 187c1da0a9..285fbce605 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 7026e6d9ce..4dedd27445 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -414,6 +414,34 @@ namespace Avalonia.Skia } } + /// + public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + { + if (rect.Height <= 0 || rect.Width <= 0) + return; + + var rc = rect.ToSKRect(); + + if (brush != null) + { + using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) + { + Canvas.DrawOval(rc, paint.Paint); + } + } + + if (pen?.Brush != null) + { + using (var paint = CreatePaint(_strokePaint, pen, rect.Size)) + { + if (paint.Paint is object) + { + Canvas.DrawOval(rc, paint.Paint); + } + } + } + } + /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 622f47f953..470157110c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -337,6 +337,45 @@ namespace Avalonia.Direct2D1.Media } } + /// + public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + { + var rc = rect.ToDirect2D(); + + if (brush != null) + { + using (var b = CreateBrush(brush, rect.Size)) + { + if (b.PlatformBrush != null) + { + _deviceContext.FillEllipse(new Ellipse + { + Point = rect.Center.ToSharpDX(), + RadiusX = (float)(rect.Width / 2), + RadiusY = (float)(rect.Height / 2) + }, b.PlatformBrush); + } + } + } + + if (pen?.Brush != null) + { + using (var wrapper = CreateBrush(pen.Brush, rect.Size)) + using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) + { + if (wrapper.PlatformBrush != null) + { + _deviceContext.DrawEllipse(new Ellipse + { + Point = rect.Center.ToSharpDX(), + RadiusX = (float)(rect.Width / 2), + RadiusY = (float)(rect.Height / 2) + }, wrapper.PlatformBrush, (float)pen.Thickness, d2dStroke); + } + } + } + } + /// /// Draws text. /// diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index 7626be7760..549f450ece 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -39,6 +39,10 @@ namespace Avalonia.Benchmarks { } + public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + { + } + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { } From 7172b921a22b59f8b009ee66a23ee6b7d83991bd Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 12 Dec 2021 17:45:35 +0100 Subject: [PATCH 2/4] Update API baseline. --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index e3f9f9a070..ee4f70e074 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -60,6 +60,7 @@ CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawEllipse(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.Rect)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract. @@ -76,4 +77,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract. -Total Issues: 77 +Total Issues: 78 From 80041d8f30e217871185782fa8365561d50f3bf7 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 12 Dec 2021 17:50:21 +0100 Subject: [PATCH 3/4] Change pointers page to use new ellipse drawing APIs. --- samples/ControlCatalog/Pages/PointersPage.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/PointersPage.cs b/samples/ControlCatalog/Pages/PointersPage.cs index 60e946dfbe..fddc503a90 100644 --- a/samples/ControlCatalog/Pages/PointersPage.cs +++ b/samples/ControlCatalog/Pages/PointersPage.cs @@ -99,10 +99,9 @@ namespace ControlCatalog.Pages foreach (var pt in _pointers.Values) { var brush = new ImmutableSolidColorBrush(pt.Color); - context.DrawGeometry(brush, null, new EllipseGeometry(new Rect(pt.Point.X - 75, pt.Point.Y - 75, - 150, 150))); + + context.DrawEllipse(brush, null, pt.Point, 75, 75); } - } } } From 500fa65b723273e0ea1d1ffd5c4ef91c405cf65b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 12 Dec 2021 18:59:53 +0100 Subject: [PATCH 4/4] Add tests for ellipse hit testing. --- .../Rendering/SceneGraph/EllipseNode.cs | 2 +- .../Rendering/SceneGraph/EllipseNodeTests.cs | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs index c817303d51..a8c5579a4b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs @@ -109,7 +109,7 @@ namespace Avalonia.Rendering.SceneGraph var distance = ry2 * dx * dx + rx2 * dy * dy; - return distance < rx2 * ry2; + return distance <= rx2 * ry2; } return false; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs new file mode 100644 index 0000000000..565b217180 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs @@ -0,0 +1,47 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Rendering.SceneGraph; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class EllipseNodeTests + { + [Theory] + [InlineData(50, 50, true)] + [InlineData(50, 0, true)] + [InlineData(100, 50, true)] + [InlineData(50, 100, true)] + [InlineData(-1, 0, false)] + [InlineData(101, 0, false)] + [InlineData(101, 101, false)] + [InlineData(0, 101, false)] + public void FillOnly_HitTest(double x, double y, bool inside) + { + var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100), null); + + var point = new Point(x, y); + + Assert.True(ellipseNode.HitTest(point) == inside); + } + + [Theory] + [InlineData(50, 0, true)] + [InlineData(51, 0, true)] + [InlineData(100, 50, true)] + [InlineData(50, 100, true)] + [InlineData(-1, 50, true)] + [InlineData(53, 50, false)] + [InlineData(101, 0, false)] + [InlineData(101, 101, false)] + [InlineData(0, 101, false)] + public void StrokeOnly_HitTest(double x, double y, bool inside) + { + var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100), null); + + var point = new Point(x, y); + + Assert.Equal(inside, ellipseNode.HitTest(point)); + } + } +}