diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
index df6b478e83..c6a3859e3d 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
@@ -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);
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/RoundedRect.cs b/src/Avalonia.Base/RoundedRect.cs
index 4c6f46ffe0..0b23a8c0ca 100644
--- a/src/Avalonia.Base/RoundedRect.cs
+++ b/src/Avalonia.Base/RoundedRect.cs
@@ -150,5 +150,64 @@ namespace Avalonia
/// For now it's internal to keep some loud community members happy about the API being pretty
///
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);
+ }
+
+ ///
+ /// Determines whether a point is in the bounds of the rounded rectangle, exclusive of the
+ /// rounded rectangle's bottom/right edge.
+ ///
+ /// The point.
+ /// true if the point is in the bounds of the rounded rectangle; otherwise false.
+ 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;
+ }
+
}
}
diff --git a/tests/Avalonia.Base.UnitTests/RoundedRectTests.cs b/tests/Avalonia.Base.UnitTests/RoundedRectTests.cs
new file mode 100644
index 0000000000..0bace3d58b
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/RoundedRectTests.cs
@@ -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)));
+ }
+
+ }
+}