浏览代码

Merge pull request #7867 from AndrejBunjac/ResolutionScalingFix

Added fixes for Margin, Padding and Thickness properties with UseLayo…
Steven Kirk 3 年之前
父节点
当前提交
f3324dc8e8

+ 56 - 3
src/Avalonia.Base/Layout/LayoutHelper.cs

@@ -52,16 +52,44 @@ namespace Avalonia.Layout
 
 
         public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding, Thickness borderThickness)
         public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding, Thickness borderThickness)
         {
         {
-            return ArrangeChild(child, availableSize, padding + borderThickness);
+            if (IsParentLayoutRounded(child, out double scale))
+            {
+                padding = RoundLayoutThickness(padding, scale, scale);
+                borderThickness = RoundLayoutThickness(borderThickness, scale, scale);
+            }
+
+            return ArrangeChildInternal(child, availableSize, padding + borderThickness);
         }
         }
 
 
         public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding)
         public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding)
+        {
+            if(IsParentLayoutRounded(child, out double scale))
+                padding = RoundLayoutThickness(padding, scale, scale);
+
+            return ArrangeChildInternal(child, availableSize, padding);
+        }
+
+        private static Size ArrangeChildInternal(ILayoutable? child, Size availableSize, Thickness padding)
         {
         {
             child?.Arrange(new Rect(availableSize).Deflate(padding));
             child?.Arrange(new Rect(availableSize).Deflate(padding));
 
 
             return availableSize;
             return availableSize;
         }
         }
 
 
+        private static bool IsParentLayoutRounded(ILayoutable? child, out double scale)
+        {
+            var layoutableParent = (ILayoutable?)child?.GetVisualParent();
+
+            if (layoutableParent == null || !((Layoutable)layoutableParent).UseLayoutRounding)
+            {
+                scale = 1.0;
+                return false;
+            }
+
+            scale = GetLayoutScale(layoutableParent);
+            return true;
+        }
+
         /// <summary>
         /// <summary>
         /// Invalidates measure for given control and all visual children recursively.
         /// Invalidates measure for given control and all visual children recursively.
         /// </summary>
         /// </summary>
@@ -126,6 +154,32 @@ namespace Avalonia.Layout
             return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
             return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
         }
         }
 
 
+        /// <summary>
+        /// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen
+        /// coordinates.
+        /// </summary>
+        /// <param name="thickness">Input thickness.</param>
+        /// <param name="dpiScaleX">DPI along x-dimension.</param>
+        /// <param name="dpiScaleY">DPI along y-dimension.</param>
+        /// <returns>Value of thickness 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 Thickness RoundLayoutThickness(Thickness thickness, double dpiScaleX, double dpiScaleY)
+        {
+            return new Thickness(
+                RoundLayoutValue(thickness.Left, dpiScaleX),
+                RoundLayoutValue(thickness.Top, dpiScaleY),
+                RoundLayoutValue(thickness.Right, dpiScaleX),
+                RoundLayoutValue(thickness.Bottom, dpiScaleY)
+            );
+        }
+
+
+
         /// <summary>
         /// <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.
         /// </summary>
         /// </summary>
@@ -163,8 +217,7 @@ namespace Avalonia.Layout
 
 
             return newValue;
             return newValue;
         }
         }
-
-
+        
         /// <summary>
         /// <summary>
         /// Calculates the min and max height for a control. Ported from WPF.
         /// Calculates the min and max height for a control. Ported from WPF.
         /// </summary>
         /// </summary>

+ 12 - 2
src/Avalonia.Base/Layout/Layoutable.cs

@@ -643,17 +643,27 @@ namespace Avalonia.Layout
         {
         {
             if (IsVisible)
             if (IsVisible)
             {
             {
+                var useLayoutRounding = UseLayoutRounding;
+                var scale = LayoutHelper.GetLayoutScale(this);
+
                 var margin = Margin;
                 var margin = Margin;
                 var originX = finalRect.X + margin.Left;
                 var originX = finalRect.X + margin.Left;
                 var originY = finalRect.Y + margin.Top;
                 var originY = finalRect.Y + margin.Top;
+
+                // Margin has to be treated separately because the layout rounding function is not linear
+                // f(a + b) != f(a) + f(b)
+                // If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales.
+                if (useLayoutRounding)
+                {
+                    margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale);
+                }
+
                 var availableSizeMinusMargins = new Size(
                 var availableSizeMinusMargins = new Size(
                     Math.Max(0, finalRect.Width - margin.Left - margin.Right),
                     Math.Max(0, finalRect.Width - margin.Left - margin.Right),
                     Math.Max(0, finalRect.Height - margin.Top - margin.Bottom));
                     Math.Max(0, finalRect.Height - margin.Top - margin.Bottom));
                 var horizontalAlignment = HorizontalAlignment;
                 var horizontalAlignment = HorizontalAlignment;
                 var verticalAlignment = VerticalAlignment;
                 var verticalAlignment = VerticalAlignment;
                 var size = availableSizeMinusMargins;
                 var size = availableSizeMinusMargins;
-                var scale = LayoutHelper.GetLayoutScale(this);
-                var useLayoutRounding = UseLayoutRounding;
 
 
                 if (horizontalAlignment != HorizontalAlignment.Stretch)
                 if (horizontalAlignment != HorizontalAlignment.Stretch)
                 {
                 {

+ 47 - 1
src/Avalonia.Controls/Border.cs

@@ -1,8 +1,10 @@
+using System;
 using Avalonia.Collections;
 using Avalonia.Collections;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Utils;
 using Avalonia.Controls.Utils;
 using Avalonia.Layout;
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 using Avalonia.VisualTree;
 
 
 namespace Avalonia.Controls
 namespace Avalonia.Controls
@@ -69,6 +71,8 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<Border, PenLineJoin>(nameof(BorderLineJoin), PenLineJoin.Miter);
             AvaloniaProperty.Register<Border, PenLineJoin>(nameof(BorderLineJoin), PenLineJoin.Miter);
 
 
         private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
         private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
+        private Thickness? _layoutThickness;
+        private double _scale;
 
 
         /// <summary>
         /// <summary>
         /// Initializes static members of the <see cref="Border"/> class.
         /// Initializes static members of the <see cref="Border"/> class.
@@ -88,6 +92,18 @@ namespace Avalonia.Controls
             AffectsMeasure<Border>(BorderThicknessProperty);
             AffectsMeasure<Border>(BorderThicknessProperty);
         }
         }
 
 
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            base.OnPropertyChanged(change);
+            switch (change.Property.Name)
+            {
+                case nameof(UseLayoutRounding):
+                case nameof(BorderThickness):
+                    _layoutThickness = null;
+                    break;
+            }
+        }
+
         /// <summary>
         /// <summary>
         /// Gets or sets a brush with which to paint the background.
         /// Gets or sets a brush with which to paint the background.
         /// </summary>
         /// </summary>
@@ -169,13 +185,43 @@ namespace Avalonia.Controls
             set => SetValue(BoxShadowProperty, value);
             set => SetValue(BoxShadowProperty, value);
         }
         }
 
 
+        private Thickness LayoutThickness
+        {
+            get
+            {
+                VerifyScale();
+
+                if (_layoutThickness == null)
+                {
+                    var borderThickness = BorderThickness;
+
+                    if (UseLayoutRounding)
+                        borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale);
+
+                    _layoutThickness = borderThickness;
+                }
+
+                return _layoutThickness.Value;
+            }
+        }
+
+        private void VerifyScale()
+        {
+            var currentScale = LayoutHelper.GetLayoutScale(this);
+            if (MathUtilities.AreClose(currentScale, _scale))
+                return;
+
+            _scale = currentScale;
+            _layoutThickness = null;
+        }
+
         /// <summary>
         /// <summary>
         /// Renders the control.
         /// Renders the control.
         /// </summary>
         /// </summary>
         /// <param name="context">The drawing context.</param>
         /// <param name="context">The drawing context.</param>
         public override void Render(DrawingContext context)
         public override void Render(DrawingContext context)
         {
         {
-            _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
+            _borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush,
                 BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray);
                 BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray);
         }
         }
 
 

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

@@ -9,6 +9,7 @@ using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.Metadata;
+using Avalonia.Utilities;
 
 
 namespace Avalonia.Controls.Presenters
 namespace Avalonia.Controls.Presenters
 {
 {
@@ -415,6 +416,10 @@ namespace Avalonia.Controls.Presenters
                 case nameof(TemplatedParent):
                 case nameof(TemplatedParent):
                     TemplatedParentChanged(change);
                     TemplatedParentChanged(change);
                     break;
                     break;
+                case nameof(UseLayoutRounding):
+                case nameof(BorderThickness):
+                    _layoutThickness = null;
+                    break;
             }
             }
         }
         }
 
 
@@ -495,10 +500,43 @@ namespace Avalonia.Controls.Presenters
             InvalidateMeasure();
             InvalidateMeasure();
         }
         }
 
 
+        private Thickness? _layoutThickness;
+        private double _scale;
+
+        private Thickness LayoutThickness
+        {
+            get
+            {
+                VerifyScale();
+
+                if (_layoutThickness == null)
+                {
+                    var borderThickness = BorderThickness;
+
+                    if (UseLayoutRounding)
+                        borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale);
+
+                    _layoutThickness = borderThickness;
+                }
+
+                return _layoutThickness.Value;
+            }
+        }
+
+        private void VerifyScale()
+        {
+            var currentScale = LayoutHelper.GetLayoutScale(this);
+            if (MathUtilities.AreClose(currentScale, _scale))
+                return;
+
+            _scale = currentScale;
+            _layoutThickness = null;
+        }
+
         /// <inheritdoc/>
         /// <inheritdoc/>
         public override void Render(DrawingContext context)
         public override void Render(DrawingContext context)
         {
         {
-            _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
+            _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush,
                 BoxShadow);
                 BoxShadow);
         }
         }
 
 
@@ -566,13 +604,22 @@ namespace Avalonia.Controls.Presenters
         {
         {
             if (Child == null) return finalSize;
             if (Child == null) return finalSize;
 
 
-            var padding = Padding + BorderThickness;
+            var useLayoutRounding = UseLayoutRounding;
+            var scale = LayoutHelper.GetLayoutScale(this);
+            var padding = Padding;
+            var borderThickness = BorderThickness;
+
+            if (useLayoutRounding)
+            {
+                padding = LayoutHelper.RoundLayoutThickness(padding, scale, scale);
+                borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale, scale);
+            }
+
+            padding += borderThickness;
             var horizontalContentAlignment = HorizontalContentAlignment;
             var horizontalContentAlignment = HorizontalContentAlignment;
             var verticalContentAlignment = VerticalContentAlignment;
             var verticalContentAlignment = VerticalContentAlignment;
-            var useLayoutRounding = UseLayoutRounding;
             var availableSize = finalSize;
             var availableSize = finalSize;
             var sizeForChild = availableSize;
             var sizeForChild = availableSize;
-            var scale = LayoutHelper.GetLayoutScale(this);
             var originX = offset.X;
             var originX = offset.X;
             var originY = offset.Y;
             var originY = offset.Y;