Browse Source

Fix UseLayoutRounding calculations.

Ported `RoundLayoutSize` and `RoundLayoutValue` from WPF and use these to calculated layout rounding.

Fixes #3467.
Steven Kirk 5 years ago
parent
commit
bd0b894271

+ 4 - 8
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -365,12 +365,8 @@ namespace Avalonia.Controls.Presenters
 
             if (useLayoutRounding)
             {
-                sizeForChild = new Size(
-                    Math.Ceiling(sizeForChild.Width * scale) / scale,
-                    Math.Ceiling(sizeForChild.Height * scale) / scale);
-                availableSize = new Size(
-                    Math.Ceiling(availableSize.Width * scale) / scale,
-                    Math.Ceiling(availableSize.Height * scale) / scale);
+                sizeForChild = LayoutHelper.RoundLayoutSize(sizeForChild, scale, scale);
+                availableSize = LayoutHelper.RoundLayoutSize(availableSize, scale, scale);
             }
 
             switch (horizontalContentAlignment)
@@ -395,8 +391,8 @@ namespace Avalonia.Controls.Presenters
 
             if (useLayoutRounding)
             {
-                originX = Math.Floor(originX * scale) / scale;
-                originY = Math.Floor(originY * scale) / scale;
+                originX = LayoutHelper.RoundLayoutValue(originX, scale);
+                originY = LayoutHelper.RoundLayoutValue(originY, scale);
             }
 
             var boundsForChild =

+ 58 - 0
src/Avalonia.Layout/LayoutHelper.cs

@@ -82,6 +82,64 @@ namespace Avalonia.Layout
             InnerInvalidateMeasure(control);
         }
 
+        /// <summary>
+        /// Rounds a size to integer values for layout purposes, compensating for high DPI screen
+        /// coordinates.
+        /// </summary>
+        /// <param name="size">Input size.</param>
+        /// <param name="dpiScaleX">DPI along x-dimension.</param>
+        /// <param name="dpiScaleY">DPI along y-dimension.</param>
+        /// <returns>Value of size that will be rounded under screen DPI.</returns>
+        /// <remarks>
+        /// This is a layout helper method. It takes DPI into account and also does not return
+        /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper
+        /// associated with the UseLayoutRounding property and should not be used as a general rounding
+        /// utility.
+        /// </remarks>
+        public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY)
+        {
+            return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
+        }
+
+        /// <summary>
+        /// Calculates the value to be used for layout rounding at high DPI.
+        /// </summary>
+        /// <param name="value">Input value to be rounded.</param>
+        /// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
+        /// <returns>Adjusted value that will produce layout rounding on screen at high dpi.</returns>
+        /// <remarks>
+        /// This is a layout helper method. It takes DPI into account and also does not return
+        /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper
+        /// associated with the UseLayoutRounding property and should not be used as a general rounding
+        /// utility.
+        /// </remarks>
+        public static double RoundLayoutValue(double value, double dpiScale)
+        {
+            double newValue;
+
+            // If DPI == 1, don't use DPI-aware rounding.
+            if (!MathUtilities.AreClose(dpiScale, 1.0))
+            {
+                newValue = Math.Round(value * dpiScale) / dpiScale;
+
+                // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue),
+                // use the original value.
+                if (double.IsNaN(newValue) ||
+                    double.IsInfinity(newValue) ||
+                    MathUtilities.AreClose(newValue, double.MaxValue))
+                {
+                    newValue = value;
+                }
+            }
+            else
+            {
+                newValue = Math.Round(value);
+            }
+
+            return newValue;
+        }
+
+
         /// <summary>
         /// Calculates the min and max height for a control. Ported from WPF.
         /// </summary>

+ 7 - 10
src/Avalonia.Layout/Layoutable.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Logging;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Layout
@@ -545,8 +546,8 @@ namespace Avalonia.Layout
                 if (UseLayoutRounding)
                 {
                     var scale = GetLayoutScale();
-                    width = Math.Ceiling(width * scale) / scale;
-                    height = Math.Ceiling(height * scale) / scale;
+                    width = LayoutHelper.RoundLayoutValue(width, scale);
+                    height = LayoutHelper.RoundLayoutValue(height, scale);
                 }
 
                 return NonNegative(new Size(width, height).Inflate(margin));
@@ -623,12 +624,8 @@ namespace Avalonia.Layout
 
                 if (useLayoutRounding)
                 {
-                    size = new Size(
-                        Math.Ceiling(size.Width * scale) / scale, 
-                        Math.Ceiling(size.Height * scale) / scale);
-                    availableSizeMinusMargins = new Size(
-                        Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale, 
-                        Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
+                    size = LayoutHelper.RoundLayoutSize(size, scale, scale);
+                    availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, scale);
                 }
 
                 size = ArrangeOverride(size).Constrain(size);
@@ -657,8 +654,8 @@ namespace Avalonia.Layout
 
                 if (useLayoutRounding)
                 {
-                    originX = Math.Floor(originX * scale) / scale;
-                    originY = Math.Floor(originY * scale) / scale;
+                    originX = LayoutHelper.RoundLayoutValue(originX, scale);
+                    originY = LayoutHelper.RoundLayoutValue(originY, scale);
                 }
 
                 Bounds = new Rect(originX, originY, size.Width, size.Height);

+ 2 - 2
tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Rect(33, 0, 34, 12), thumb.Bounds);
+            Assert.Equal(new Rect(33, 0, 33, 12), thumb.Bounds);
         }
 
         [Fact]
@@ -92,7 +92,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.Measure(new Size(100, 100));
             target.Arrange(new Rect(0, 0, 100, 100));
 
-            Assert.Equal(new Rect(0, 33, 12, 34), thumb.Bounds);
+            Assert.Equal(new Rect(0, 33, 12, 33), thumb.Bounds);
         }
 
         [Fact]

BIN
tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png


BIN
tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png