diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index 404d19906a..0851dbaea9 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -251,6 +251,17 @@ namespace Avalonia.Layout { double newValue; + // Round the value to avoid FP errors. This is needed because if `value` has a floating + // point precision error (e.g. 79.333333333333343) then when it's multiplied by + // `dpiScale` and rounded up, it will be rounded up to a value one greater than it + // should be. +#if NET6_0_OR_GREATER + value = Math.Round(value, 8, MidpointRounding.ToZero); +#else + // MidpointRounding.ToZero isn't available in netstandard2.0. + value = Math.Truncate(value * 1e8) / 1e8; +#endif + // If DPI == 1, don't use DPI-aware rounding. if (!MathUtilities.IsOne(dpiScale)) { diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index ecc7cd98bb..eb9f38894d 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Utils _backgroundGeometryCache = null; } - if (boundRect.Width != 0 && innerRect.Height != 0) + if (boundRect.Width != 0 && boundRect.Height != 0) { var borderGeometryKeypoints = new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false); diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs index 7af7d1cee2..e9ddc8e8a3 100644 --- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/BorderTests.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.UnitTests } }; - var root = CreatedRoot(1.5, target); + var root = CreateRoot(1.5, target); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -101,7 +101,7 @@ namespace Avalonia.Controls.UnitTests } }; - var root = CreatedRoot(1.5, target); + var root = CreateRoot(1.5, target); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -111,7 +111,58 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(104, 104), target.DesiredSize); } - private static TestRoot CreatedRoot( + [Fact] + public void Measure_Arranges_Child_To_Rounded_BorderThickness() + { + Canvas child; + var target = new Border + { + BorderThickness = new Thickness(1), + Width = 82, + Height = 82, + Child = child = new Canvas(), + }; + + var root = CreateRoot(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 82 needs no rounding + // - Minus border thickness, space for child is 82 - 2.6666 = 79.3333 + Assert.Equal(1.3333, child.Bounds.Left, 3); + Assert.Equal(1.3333, child.Bounds.Top, 3); + Assert.Equal(79.3333, child.Bounds.Width, 3); + Assert.Equal(79.3333, child.Bounds.Height, 3); + } + + [Fact] + public void Measure_Arranges_Child_With_Rounded_Margin() + { + Border child; + var target = new Border + { + Width = 220, + Height = 220, + Child = child = new Border + { + Margin = new Thickness(0, 25, 25, 25), + }, + }; + + var root = CreateRoot(1.5, target); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + // - 25 margin gets rounded up to 25.3333 + // - Size of 220 needs no rounding + Assert.Equal(0, child.Bounds.Left, 3); + Assert.Equal(25.3333, child.Bounds.Top, 3); + Assert.Equal(194.6666, child.Bounds.Width, 3); + Assert.Equal(169.3333, child.Bounds.Height, 3); + } + + private static TestRoot CreateRoot( double scaling, Control child, Size? constraint = null)