Browse Source

Merge pull request #7031 from MarchingCube/dc-ellipse

Add support for drawing ellipses directly via DrawingContext
# Conflicts:
#	src/Avalonia.Visuals/ApiCompatBaseline.txt
release/0.10.11
Dan Walmsley 4 years ago
parent
commit
918c47d6b8
  1. 5
      samples/ControlCatalog/Pages/PointersPage.cs
  2. 4
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  3. 3
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  4. 28
      src/Avalonia.Visuals/Media/DrawingContext.cs
  5. 12
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  6. 14
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  7. 118
      src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
  8. 3
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  9. 28
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  10. 39
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  11. 4
      tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
  12. 47
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs

5
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);
}
}
}
}

4
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -448,6 +448,10 @@ namespace Avalonia.Headless
}
public void DrawEllipse(IBrush brush, IPen pen, Rect rect)
{
}
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{

3
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -5,6 +5,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist 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.
@ -18,4 +19,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: 19
Total Issues: 20

28
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);
}
/// <summary>
/// Draws an ellipse with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the ellipse, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the ellipse, or <c>null</c> for no stroke.</param>
/// <param name="center">The location of the center of the ellipse.</param>
/// <param name="radiusX">The horizontal radius of the ellipse.</param>
/// <param name="radiusY">The vertical radius of the ellipse.</param>
/// <remarks>
/// 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.
/// </remarks>
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));
}
/// <summary>
/// Draws a custom drawing operation
/// </summary>

12
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -71,6 +71,18 @@ namespace Avalonia.Platform
void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect,
BoxShadows boxShadows = default);
/// <summary>
/// Draws an ellipse with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the ellipse, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the ellipse, or <c>null</c> for no stroke.</param>
/// <param name="rect">The ellipse bounds.</param>
/// <remarks>
/// 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.
/// </remarks>
void DrawEllipse(IBrush brush, IPen pen, Rect rect);
/// <summary>
/// Draws text.
/// </summary>

14
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<EllipseNode>();
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<CustomDrawOperation>();

118
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
{
/// <summary>
/// A node in the scene graph which represents an ellipse draw.
/// </summary>
internal class EllipseNode : BrushDrawOperation
{
public EllipseNode(
Matrix transform,
IBrush brush,
IPen pen,
Rect rect,
IDictionary<IVisual, Scene> childScenes = null)
: base(rect.Inflate(pen?.Thickness ?? 0), transform)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
ChildScenes = childScenes;
}
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush Brush { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
public ImmutablePen Pen { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the rect of the ellipse to draw.
/// </summary>
public Rect Rect { get; }
public override IDictionary<IVisual, Scene> 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;
}
}
}

3
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;

28
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -414,6 +414,34 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
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);
}
}
}
}
/// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{

39
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -337,6 +337,45 @@ namespace Avalonia.Direct2D1.Media
}
}
/// <inheritdoc />
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);
}
}
}
}
/// <summary>
/// Draws text.
/// </summary>

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

47
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));
}
}
}
Loading…
Cancel
Save