浏览代码

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.
Bill Henning 1 年之前
父节点
当前提交
47fb1d94b3

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

+ 59 - 0
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 
         /// </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;
+        }
+
     }
 }

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