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