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