Updated RenderDataRectangleNode.HitTest to properly hit-test rounded rectangles (#13797)
* Updated RenderDataRectangleNode.HitTest to properly hit-test rounded rectangles. * Moved rounded rectangle contains logic to the RoundedRect struct, added unit tests, and refactored previous RenderDataRectangleNode changes. * Fixed a comment typo. * Added a private access modifier to a method.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
|
||||
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
|
||||
|
||||
@@ -7,24 +6,41 @@ class RenderDataRectangleNode : RenderDataBrushAndPenNode
|
||||
{
|
||||
public RoundedRect Rect { get; set; }
|
||||
public BoxShadows BoxShadows { get; set; }
|
||||
|
||||
|
||||
public override bool HitTest(Point p)
|
||||
{
|
||||
if (ServerBrush != null) // it's safe to check for null
|
||||
var strokeThicknessAdjustment = (ClientPen?.Thickness / 2) ?? 0;
|
||||
|
||||
if (Rect.IsRounded)
|
||||
{
|
||||
var rect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0);
|
||||
return rect.ContainsExclusive(p);
|
||||
var outerRoundedRect = Rect.Inflate(strokeThicknessAdjustment, strokeThicknessAdjustment);
|
||||
if (outerRoundedRect.ContainsExclusive(p))
|
||||
{
|
||||
if (ServerBrush != null) // it's safe to check for null
|
||||
return true;
|
||||
|
||||
var innerRoundedRect = Rect.Deflate(strokeThicknessAdjustment, strokeThicknessAdjustment);
|
||||
return !innerRoundedRect.ContainsExclusive(p);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var borderRect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0);
|
||||
var emptyRect = Rect.Rect.Deflate((ClientPen?.Thickness / 2) ?? 0);
|
||||
return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
|
||||
var outerRect = Rect.Rect.Inflate(strokeThicknessAdjustment);
|
||||
if (outerRect.ContainsExclusive(p))
|
||||
{
|
||||
if (ServerBrush != null) // it's safe to check for null
|
||||
return true;
|
||||
|
||||
var innerRect = Rect.Rect.Deflate(strokeThicknessAdjustment);
|
||||
return !innerRect.ContainsExclusive(p);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Invoke(ref RenderDataNodeRenderContext context) =>
|
||||
context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows);
|
||||
|
||||
public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,5 +150,64 @@ namespace Avalonia
|
||||
/// For now it's internal to keep some loud community members happy about the API being pretty
|
||||
/// </summary>
|
||||
internal bool IsEmpty() => this == default;
|
||||
|
||||
private static bool IsOutsideCorner(double dx, double dy, double radius)
|
||||
{
|
||||
return (dx < 0) && (dy < 0) && (dx * dx + dy * dy > radius * radius);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a point is in the bounds of the rounded rectangle, exclusive of the
|
||||
/// rounded rectangle's bottom/right edge.
|
||||
/// </summary>
|
||||
/// <param name="p">The point.</param>
|
||||
/// <returns>true if the point is in the bounds of the rounded rectangle; otherwise false.</returns>
|
||||
public bool ContainsExclusive(Point p)
|
||||
{
|
||||
// Do a simple rectangular bounds check first
|
||||
if (!Rect.ContainsExclusive(p))
|
||||
return false;
|
||||
|
||||
// If any radii totals exceed available bounds, determine a scale factor that needs to be applied
|
||||
var scaleFactor = 1.0;
|
||||
if (Rect.Width > 0)
|
||||
{
|
||||
var radiiWidth = Math.Max(RadiiTopLeft.X + RadiiTopRight.X, RadiiBottomLeft.X + RadiiBottomRight.X);
|
||||
if (radiiWidth > Rect.Width)
|
||||
scaleFactor = Math.Min(scaleFactor, Rect.Width / radiiWidth);
|
||||
}
|
||||
if (Rect.Height > 0)
|
||||
{
|
||||
var radiiHeight = Math.Max(RadiiTopLeft.Y + RadiiBottomLeft.Y, RadiiTopRight.Y + RadiiBottomRight.Y);
|
||||
if (radiiHeight > Rect.Height)
|
||||
scaleFactor = Math.Min(scaleFactor, Rect.Height / radiiHeight);
|
||||
}
|
||||
|
||||
// Before corner hit-testing, make the point relative to the bounds' upper-left
|
||||
p = new Point(p.X - Rect.X, p.Y - Rect.Y);
|
||||
|
||||
// Top-left corner
|
||||
var radius = Math.Min(RadiiTopLeft.X, RadiiTopLeft.Y) * scaleFactor;
|
||||
if (IsOutsideCorner(p.X - radius, p.Y - radius, radius))
|
||||
return false;
|
||||
|
||||
// Top-right corner
|
||||
radius = Math.Min(RadiiTopRight.X, RadiiTopRight.Y) * scaleFactor;
|
||||
if (IsOutsideCorner(Rect.Width - radius - p.X, p.Y - radius, radius))
|
||||
return false;
|
||||
|
||||
// Bottom-right corner
|
||||
radius = Math.Min(RadiiBottomRight.X, RadiiBottomRight.Y) * scaleFactor;
|
||||
if (IsOutsideCorner(Rect.Width - radius - p.X, Rect.Height - radius - p.Y, radius))
|
||||
return false;
|
||||
|
||||
// Bottom-left corner
|
||||
radius = Math.Min(RadiiBottomLeft.X, RadiiBottomLeft.Y) * scaleFactor;
|
||||
if (IsOutsideCorner(p.X - radius, Rect.Height - radius - p.Y, radius))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.Base.UnitTests
|
||||
{
|
||||
public class RoundedRectTests
|
||||
{
|
||||
|
||||
[Theory,
|
||||
// Corners
|
||||
InlineData(0, 0, false),
|
||||
InlineData(100, 0, false),
|
||||
InlineData(100, 100, false),
|
||||
InlineData(0, 100, false),
|
||||
// Indent 10px
|
||||
InlineData(10, 10, false),
|
||||
InlineData(90, 10, true),
|
||||
InlineData(90, 90, false),
|
||||
InlineData(10, 90, true),
|
||||
// Indent 17px
|
||||
InlineData(17, 17, false),
|
||||
InlineData(83, 17, true),
|
||||
InlineData(83, 83, true),
|
||||
InlineData(17, 83, true),
|
||||
// Center
|
||||
InlineData(50, 50, true),
|
||||
]
|
||||
public void ContainsExclusive_Should_Return_Expected_Result_For_Point(double x, double y, bool expectedResult)
|
||||
{
|
||||
var rrect = new RoundedRect(new Rect(0, 0, 100, 100), new CornerRadius(60, 10, 50, 30));
|
||||
|
||||
Assert.Equal(expectedResult, rrect.ContainsExclusive(new Point(x, y)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user