Browse Source

Merge pull request #8146 from AvaloniaUI/fixes/8092-uselayoutrounding-fixes

Fix problems with UseLayoutRounding.
Max Katz 3 năm trước cách đây
mục cha
commit
b46d4390c6

+ 63 - 8
src/Avalonia.Base/Layout/LayoutHelper.cs

@@ -36,11 +36,28 @@ namespace Avalonia.Layout
         public static Size MeasureChild(ILayoutable? control, Size availableSize, Thickness padding,
             Thickness borderThickness)
         {
-            return MeasureChild(control, availableSize, padding + borderThickness);
+            if (IsParentLayoutRounded(control, out double scale))
+            {
+                padding = RoundLayoutThickness(padding, scale, scale);
+                borderThickness = RoundLayoutThickness(borderThickness, scale, scale);
+            }
+
+            if (control != null)
+            {
+                control.Measure(availableSize.Deflate(padding + borderThickness));
+                return control.DesiredSize.Inflate(padding + borderThickness);
+            }
+
+            return new Size().Inflate(padding + borderThickness);
         }
 
         public static Size MeasureChild(ILayoutable? control, Size availableSize, Thickness padding)
         {
+            if (IsParentLayoutRounded(control, out double scale))
+            {
+                padding = RoundLayoutThickness(padding, scale, scale);
+            }
+
             if (control != null)
             {
                 control.Measure(availableSize.Deflate(padding));
@@ -137,7 +154,7 @@ namespace Avalonia.Layout
 
         /// <summary>
         /// Rounds a size to integer values for layout purposes, compensating for high DPI screen
-        /// coordinates.
+        /// coordinates by rounding the size up to the nearest pixel.
         /// </summary>
         /// <param name="size">Input size.</param>
         /// <param name="dpiScaleX">DPI along x-dimension.</param>
@@ -149,9 +166,9 @@ namespace Avalonia.Layout
         /// 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)
+        public static Size RoundLayoutSizeUp(Size size, double dpiScaleX, double dpiScaleY)
         {
-            return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
+            return new Size(RoundLayoutValueUp(size.Width, dpiScaleX), RoundLayoutValueUp(size.Height, dpiScaleY));
         }
 
         /// <summary>
@@ -178,10 +195,9 @@ namespace Avalonia.Layout
             );
         }
 
-
-
         /// <summary>
-        /// Calculates the value to be used for layout rounding at high DPI.
+        /// Calculates the value to be used for layout rounding at high DPI by rounding the value
+        /// up or down to the nearest pixel.
         /// </summary>
         /// <param name="value">Input value to be rounded.</param>
         /// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
@@ -217,7 +233,46 @@ namespace Avalonia.Layout
 
             return newValue;
         }
-        
+
+        /// <summary>
+        /// Calculates the value to be used for layout rounding at high DPI by rounding the value up
+        /// to the nearest pixel.
+        /// </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 RoundLayoutValueUp(double value, double dpiScale)
+        {
+            double newValue;
+
+            // If DPI == 1, don't use DPI-aware rounding.
+            if (!MathUtilities.IsOne(dpiScale))
+            {
+                newValue = Math.Ceiling(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.Ceiling(value);
+            }
+
+            return newValue;
+        }
+
         /// <summary>
         /// Calculates the min and max height for a control. Ported from WPF.
         /// </summary>

+ 15 - 9
src/Avalonia.Base/Layout/Layoutable.cs

@@ -548,6 +548,14 @@ namespace Avalonia.Layout
             if (IsVisible)
             {
                 var margin = Margin;
+                var useLayoutRounding = UseLayoutRounding;
+                var scale = 1.0;
+
+                if (useLayoutRounding)
+                {
+                    scale = LayoutHelper.GetLayoutScale(this);
+                    margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale);
+                }
 
                 ApplyStyling();
                 ApplyTemplate();
@@ -584,16 +592,14 @@ namespace Avalonia.Layout
                 height = Math.Min(height, MaxHeight);
                 height = Math.Max(height, MinHeight);
 
-                width = Math.Min(width, availableSize.Width);
-                height = Math.Min(height, availableSize.Height);
-
-                if (UseLayoutRounding)
+                if (useLayoutRounding)
                 {
-                    var scale = LayoutHelper.GetLayoutScale(this);
-                    width = LayoutHelper.RoundLayoutValue(width, scale);
-                    height = LayoutHelper.RoundLayoutValue(height, scale);
+                    (width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale, scale);
                 }
 
+                width = Math.Min(width, availableSize.Width);
+                height = Math.Min(height, availableSize.Height);
+
                 return NonNegative(new Size(width, height).Inflate(margin));
             }
             else
@@ -678,8 +684,8 @@ namespace Avalonia.Layout
 
                 if (useLayoutRounding)
                 {
-                    size = LayoutHelper.RoundLayoutSize(size, scale, scale);
-                    availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, scale);
+                    size = LayoutHelper.RoundLayoutSizeUp(size, scale, scale);
+                    availableSizeMinusMargins = LayoutHelper.RoundLayoutSizeUp(availableSizeMinusMargins, scale, scale);
                 }
 
                 size = ArrangeOverride(size).Constrain(size);

+ 13 - 1
src/Avalonia.Base/Point.cs

@@ -188,7 +188,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Returns a boolean indicating whether the point is equal to the other given point.
+        /// Returns a boolean indicating whether the point is equal to the other given point (bitwise).
         /// </summary>
         /// <param name="other">The other point to test equality against.</param>
         /// <returns>True if this point is equal to other; False otherwise.</returns>
@@ -200,6 +200,18 @@ namespace Avalonia
             // ReSharper enable CompareOfFloatsByEqualityOperator
         }
 
+        /// <summary>
+        /// Returns a boolean indicating whether the point is equal to the other given point
+        /// (numerically).
+        /// </summary>
+        /// <param name="other">The other point to test equality against.</param>
+        /// <returns>True if this point is equal to other; False otherwise.</returns>
+        public bool NearlyEquals(Point other)
+        {
+            return MathUtilities.AreClose(_x, other._x) &&
+                   MathUtilities.AreClose(_y, other._y);
+        }
+
         /// <summary>
         /// Checks for equality between a point and an object.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -855,7 +855,7 @@ namespace Avalonia.Controls
             if (OwningGrid != null && OwningGrid.UseLayoutRounding)
             {
                 var scale = LayoutHelper.GetLayoutScale(HeaderCell);
-                var roundSize = LayoutHelper.RoundLayoutSize(new Size(leftEdge + ActualWidth, 1), scale, scale);
+                var roundSize = LayoutHelper.RoundLayoutSizeUp(new Size(leftEdge + ActualWidth, 1), scale, scale);
                 LayoutRoundedWidth = roundSize.Width - leftEdge;
             }
             else

+ 1 - 1
src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs

@@ -26,7 +26,7 @@ namespace Avalonia.Controls.Converters
 
             if (visibility == ScrollBarVisibility.Auto)
             {
-                if (extent == viewport)
+                if (MathUtilities.AreClose(extent, viewport))
                 {
                     return false;
                 }

+ 2 - 2
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -635,8 +635,8 @@ namespace Avalonia.Controls.Presenters
 
             if (useLayoutRounding)
             {
-                sizeForChild = LayoutHelper.RoundLayoutSize(sizeForChild, scale, scale);
-                availableSize = LayoutHelper.RoundLayoutSize(availableSize, scale, scale);
+                sizeForChild = LayoutHelper.RoundLayoutSizeUp(sizeForChild, scale, scale);
+                availableSize = LayoutHelper.RoundLayoutSizeUp(availableSize, scale, scale);
             }
 
             switch (horizontalContentAlignment)

+ 0 - 31
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests.cs

@@ -173,37 +173,6 @@ namespace Avalonia.Base.UnitTests.Layout
             target.Verify(x => x.InvalidateMeasure(root), Times.Once());
         }
 
-        [Theory]
-        [InlineData(16, 6, 5.333333333333333)]
-        [InlineData(18, 10, 4)]
-        public void UseLayoutRounding_Arranges_Center_Alignment_Correctly_With_Fractional_Scaling(
-            double containerWidth,
-            double childWidth,
-            double expectedX)
-        {
-            Border target;
-            var root = new TestRoot
-            {
-                LayoutScaling = 1.5,
-                UseLayoutRounding = true,
-                Child = new Decorator
-                {
-                    Width = containerWidth,
-                    Height = 100,
-                    Child = target = new Border
-                    {
-                        Width = childWidth,
-                        HorizontalAlignment = HorizontalAlignment.Center,
-                    }
-                }
-            };
-
-            root.Measure(new Size(100, 100));
-            root.Arrange(new Rect(target.DesiredSize));
-
-            Assert.Equal(new Rect(expectedX, 0, childWidth, 100), target.Bounds);
-        }
-
         [Fact]
         public void LayoutUpdated_Is_Called_At_End_Of_Layout_Pass()
         {

+ 140 - 0
tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_LayoutRounding.cs

@@ -0,0 +1,140 @@
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.UnitTests;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Avalonia.Base.UnitTests.Layout
+{
+    public class LayoutableTests_LayoutRounding
+    {
+        [Theory]
+        [InlineData(100, 100)]
+        [InlineData(101, 101.33333333333333)]
+        [InlineData(103, 103.33333333333333)]
+        public void Measure_Adjusts_DesiredSize_Upwards_When_Constraint_Allows(double desiredSize, double expectedSize)
+        {
+            var target = new TestLayoutable(new Size(desiredSize, desiredSize));
+            var root = CreateRoot(1.5, target);
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            Assert.Equal(new Size(expectedSize, expectedSize), target.DesiredSize);
+        }
+
+        [Fact]
+        public void Measure_Constrains_Adjusted_DesiredSize_To_Constraint()
+        {
+            var target = new TestLayoutable(new Size(101, 101));
+            var root = CreateRoot(1.5, target, constraint: new Size(101, 101));
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            // Desired width/height with layout rounding is 101.3333 but constraint is 101,101 so
+            // layout rounding should be ignored.
+            Assert.Equal(new Size(101, 101), target.DesiredSize);
+        }
+
+        [Fact]
+        public void Measure_Adjusts_DesiredSize_Upwards_When_Margin_Present()
+        {
+            var target = new TestLayoutable(new Size(101, 101), margin: 1);
+            var root = CreateRoot(1.5, target);
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            // - 1 pixel margin is rounded up to 1.3333; for both sides it is 2.6666
+            // - Size of 101 gets rounded up to 101.3333
+            // - Final size = 101.3333 + 2.6666 = 104
+            AssertEqual(new Size(104, 104), target.DesiredSize);
+        }
+
+        [Fact]
+        public void Arrange_Adjusts_Bounds_Upwards_With_Margin()
+        {
+            var target = new TestLayoutable(new Size(101, 101), margin: 1);
+            var root = CreateRoot(1.5, target);
+
+            root.LayoutManager.ExecuteInitialLayoutPass();
+
+            // - 1 pixel margin is rounded up to 1.3333
+            // - Size of 101 gets rounded up to 101.3333
+            AssertEqual(new Point(1.3333333333333333, 1.3333333333333333), target.Bounds.Position);
+            AssertEqual(new Size(101.33333333333333, 101.33333333333333), target.Bounds.Size);
+        }
+
+        [Theory]
+        [InlineData(16, 6, 5.333333333333333)]
+        [InlineData(18, 10, 4)]
+        public void Arranges_Center_Alignment_Correctly_With_Fractional_Scaling(
+            double containerWidth,
+            double childWidth,
+            double expectedX)
+        {
+            Border target;
+            var root = new TestRoot
+            {
+                LayoutScaling = 1.5,
+                UseLayoutRounding = true,
+                Child = new Decorator
+                {
+                    Width = containerWidth,
+                    Height = 100,
+                    Child = target = new Border
+                    {
+                        Width = childWidth,
+                        HorizontalAlignment = HorizontalAlignment.Center,
+                    }
+                }
+            };
+
+            root.Measure(new Size(100, 100));
+            root.Arrange(new Rect(target.DesiredSize));
+
+            Assert.Equal(new Rect(expectedX, 0, childWidth, 100), target.Bounds);
+        }
+
+        private static TestRoot CreateRoot(
+            double scaling,
+            Control child,
+            Size? constraint = null)
+        {
+            return new TestRoot
+            {
+                LayoutScaling = scaling,
+                UseLayoutRounding = true,
+                Child = child,
+                ClientSize = constraint ?? new Size(1000, 1000),
+            };
+        }
+
+        private static void AssertEqual(Point expected, Point actual)
+        {
+            if (!expected.NearlyEquals(actual))
+            {
+                throw new EqualException(expected, actual);
+            }
+        }
+
+        private static void AssertEqual(Size expected, Size actual)
+        {
+            if (!expected.NearlyEquals(actual))
+            {
+                throw new EqualException(expected, actual);
+            }
+        }
+
+        private class TestLayoutable : Control
+        {
+            private Size _desiredSize;
+
+            public TestLayoutable(Size desiredSize, double margin = 0)
+            {
+                _desiredSize = desiredSize;
+                Margin = new Thickness(margin);
+            }
+
+            protected override Size MeasureOverride(Size availableSize) => _desiredSize;
+        }
+    }
+}

+ 1 - 0
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -838,6 +838,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
                 Canvas canvas;
                 var tree = new TestRoot
                 {
+                    ClientSize = new Size(100, 100),
                     Child = decorator = new Decorator
                     {
                         Margin = new Thickness(0, 10, 0, 0),

+ 65 - 0
tests/Avalonia.Controls.UnitTests/BorderTests.cs

@@ -1,6 +1,8 @@
+using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
+using Avalonia.VisualTree;
 using Moq;
 using Xunit;
 
@@ -60,5 +62,68 @@ namespace Avalonia.Controls.UnitTests
 
             renderer.Verify(x => x.AddDirty(target), Times.Once);
         }
+
+        public class UseLayoutRounding
+        {
+            [Fact]
+            public void Measure_Rounds_Padding()
+            {
+                var target = new Border 
+                { 
+                    Padding = new Thickness(1),
+                    Child = new Canvas
+                    {
+                        Width = 101,
+                        Height = 101,
+                    }
+                };
+
+                var root = CreatedRoot(1.5, target);
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                // - 1 pixel padding is rounded up to 1.3333; for both sides it is 2.6666
+                // - Size of 101 gets rounded up to 101.3333
+                // - Desired size = 101.3333 + 2.6666 = 104
+                Assert.Equal(new Size(104, 104), target.DesiredSize);
+            }
+
+            [Fact]
+            public void Measure_Rounds_BorderThickness()
+            {
+                var target = new Border
+                {
+                    BorderThickness = new Thickness(1),
+                    Child = new Canvas
+                    {
+                        Width = 101,
+                        Height = 101,
+                    }
+                };
+
+                var root = CreatedRoot(1.5, target);
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                // - 1 pixel border thickness is rounded up to 1.3333; for both sides it is 2.6666
+                // - Size of 101 gets rounded up to 101.3333
+                // - Desired size = 101.3333 + 2.6666 = 104
+                Assert.Equal(new Size(104, 104), target.DesiredSize);
+            }
+
+            private static TestRoot CreatedRoot(
+                double scaling,
+                Control child,
+                Size? constraint = null)
+            {
+                return new TestRoot
+                {
+                    LayoutScaling = scaling,
+                    UseLayoutRounding = true,
+                    Child = child,
+                    ClientSize = constraint ?? new Size(1000, 1000),
+                };
+            }
+        }
     }
 }

+ 41 - 0
tests/Avalonia.Controls.UnitTests/DecoratorTests.cs

@@ -1,6 +1,7 @@
 using System.Collections.Specialized;
 using System.Linq;
 using Avalonia.LogicalTree;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -116,5 +117,45 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal(new Size(16, 16), target.DesiredSize);
         }
+
+        public class UseLayoutRounding
+        {
+            [Fact]
+            public void Measure_Rounds_Padding()
+            {
+                var target = new Decorator
+                {
+                    Padding = new Thickness(1),
+                    Child = new Canvas
+                    {
+                        Width = 101,
+                        Height = 101,
+                    }
+                };
+
+                var root = CreatedRoot(1.5, target);
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                // - 1 pixel padding is rounded up to 1.3333; for both sides it is 2.6666
+                // - Size of 101 gets rounded up to 101.3333
+                // - Desired size = 101.3333 + 2.6666 = 104
+                Assert.Equal(new Size(104, 104), target.DesiredSize);
+            }
+
+            private static TestRoot CreatedRoot(
+                double scaling,
+                Control child,
+                Size? constraint = null)
+            {
+                return new TestRoot
+                {
+                    LayoutScaling = scaling,
+                    UseLayoutRounding = true,
+                    Child = child,
+                    ClientSize = constraint ?? new Size(1000, 1000),
+                };
+            }
+        }
     }
 }

+ 65 - 1
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@@ -1,5 +1,6 @@
 using Avalonia.Controls.Presenters;
 using Avalonia.Layout;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Presenters
@@ -232,5 +233,68 @@ namespace Avalonia.Controls.UnitTests.Presenters
 
             Assert.Equal(new Rect(32, 32, 0, 0), content.Bounds);
         }
+
+        public class UseLayoutRounding
+        {
+            [Fact]
+            public void Measure_Rounds_Padding()
+            {
+                var target = new ContentPresenter
+                {
+                    Padding = new Thickness(1),
+                    Content = new Canvas
+                    {
+                        Width = 101,
+                        Height = 101,
+                    }
+                };
+
+                var root = CreatedRoot(1.5, target);
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                // - 1 pixel padding is rounded up to 1.3333; for both sides it is 2.6666
+                // - Size of 101 gets rounded up to 101.3333
+                // - Desired size = 101.3333 + 2.6666 = 104
+                Assert.Equal(new Size(104, 104), target.DesiredSize);
+            }
+
+            [Fact]
+            public void Measure_Rounds_BorderThickness()
+            {
+                var target = new ContentPresenter
+                {
+                    BorderThickness = new Thickness(1),
+                    Content = new Canvas
+                    {
+                        Width = 101,
+                        Height = 101,
+                    }
+                };
+
+                var root = CreatedRoot(1.5, target);
+
+                root.LayoutManager.ExecuteInitialLayoutPass();
+
+                // - 1 pixel border thickness is rounded up to 1.3333; for both sides it is 2.6666
+                // - Size of 101 gets rounded up to 101.3333
+                // - Desired size = 101.3333 + 2.6666 = 104
+                Assert.Equal(new Size(104, 104), target.DesiredSize);
+            }
+
+            private static TestRoot CreatedRoot(
+                double scaling,
+                Control child,
+                Size? constraint = null)
+            {
+                return new TestRoot
+                {
+                    LayoutScaling = scaling,
+                    UseLayoutRounding = true,
+                    Child = child,
+                    ClientSize = constraint ?? new Size(1000, 1000),
+                };
+            }
+        }
     }
-}
+}

+ 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, 33, 12), thumb.Bounds);
+            Assert.Equal(new Rect(33, 0, 34, 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, 33), thumb.Bounds);
+            Assert.Equal(new Rect(0, 33, 12, 34), thumb.Bounds);
         }
 
         [Fact]

+ 6 - 1
tests/Avalonia.UnitTests/TestRoot.cs

@@ -41,7 +41,7 @@ namespace Avalonia.UnitTests
             Child = child;
         }
 
-        public Size ClientSize { get; set; } = new Size(100, 100);
+        public Size ClientSize { get; set; } = new Size(1000, 1000);
 
         public Size MaxClientSize { get; set; } = Size.Infinity;
 
@@ -110,5 +110,10 @@ namespace Avalonia.UnitTests
             }
             Visit(this, true);
         }
+
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            return base.MeasureOverride(ClientSize);
+        }
     }
 }

BIN
tests/TestFiles/Direct2D1/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png


BIN
tests/TestFiles/Skia/Controls/TextBlock/RestrictedHeight_VerticalAlign.expected.png