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) { }