diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index b9075b957b..8fc91aca14 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -1,6 +1,7 @@ false + $(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 50aa8a9e71..f75184c686 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/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 = diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index 2b61de00a7..d8fa00deb7 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -82,6 +82,64 @@ namespace Avalonia.Layout InnerInvalidateMeasure(control); } + /// + /// Rounds a size to integer values for layout purposes, compensating for high DPI screen + /// coordinates. + /// + /// Input size. + /// DPI along x-dimension. + /// DPI along y-dimension. + /// Value of size that will be rounded under screen DPI. + /// + /// 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. + /// + public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY) + { + return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY)); + } + + /// + /// Calculates the value to be used for layout rounding at high DPI. + /// + /// Input value to be rounded. + /// Ratio of screen's DPI to layout DPI + /// Adjusted value that will produce layout rounding on screen at high dpi. + /// + /// 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. + /// + 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; + } + + /// /// Calculates the min and max height for a control. Ported from WPF. /// diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index cbeab0482e..ce5200f4a4 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/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); diff --git a/src/Avalonia.Themes.Default/ScrollBar.xaml b/src/Avalonia.Themes.Default/ScrollBar.xaml index 64a4399d16..0f8fa4986d 100644 --- a/src/Avalonia.Themes.Default/ScrollBar.xaml +++ b/src/Avalonia.Themes.Default/ScrollBar.xaml @@ -3,7 +3,8 @@ - + - + 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); + } + private class TestLayoutable : Layoutable { public Size ArrangeSize { get; private set; } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 56a1a6c245..f291d386aa 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -42,7 +42,7 @@ namespace Avalonia.UnitTests public Size MaxClientSize { get; set; } = Size.Infinity; - public double LayoutScaling => 1; + public double LayoutScaling { get; set; } = 1; public ILayoutManager LayoutManager { get; set; } = new LayoutManager(); diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png index 3996782889..ea81296017 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png index f93814556e..a8ec09df99 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png index 84b76e795e..485b03acd3 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png index f2346a64a0..a6bc0daee3 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png index cb355b9ce0..8c622c37b5 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png index 08f41b3ef5..bfe5fd7fbb 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png index a0b6afeeeb..f3c3c4aee2 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png index 1d620ae100..80116d81c6 100644 Binary files a/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Direct2D1/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png index 4a7f4c26b0..7a7d70a521 100644 Binary files a/tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png and b/tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png index 078bca57bc..474e2bb2bf 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_Fill_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png index f93814556e..4ca25db112 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_InTree_Visual.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png index 84b76e795e..3c9c88e8f1 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipXY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png index f2346a64a0..e9efe1e796 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipX_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png index cb355b9ce0..b864ffca33 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_FlipY_TopLeftDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png index 08f41b3ef5..fa359eac11 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_Alignment_Center.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png index a0b6afeeeb..778e3f6c13 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_NoStretch_NoTile_BottomRightQuarterDest.expected.png differ diff --git a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png index 1f1ac05853..d7ef920967 100644 Binary files a/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png and b/tests/TestFiles/Skia/Media/VisualBrush/VisualBrush_UniformToFill_NoTile.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png b/tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png index 4a7f4c26b0..7727284050 100644 Binary files a/tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png and b/tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png differ