From 9ad87658f194757a66b62251f2ebb02308035dd9 Mon Sep 17 00:00:00 2001 From: Andrej Bunjac Date: Thu, 24 Mar 2022 14:16:18 +0100 Subject: [PATCH 1/3] Added fixes for Margin, Padding and Thickness properties with UseLayoutRounding = true. --- src/Avalonia.Controls/Border.cs | 25 +++++++- .../Presenters/ContentPresenter.cs | 40 +++++++++++-- src/Avalonia.Layout/LayoutHelper.cs | 59 ++++++++++++++++++- src/Avalonia.Layout/Layoutable.cs | 14 ++++- 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index ee3be1d5b33..ce64570dc8c 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -169,13 +169,36 @@ public BoxShadows BoxShadow set => SetValue(BoxShadowProperty, value); } + private Thickness _layoutThickness = default; + + private Thickness LayoutThickness + { + get + { + if (_layoutThickness == default) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + { + var scale = LayoutHelper.GetLayoutScale(this); + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); + } + + _layoutThickness = borderThickness; + } + + return _layoutThickness; + } + } + /// /// Renders the control. /// /// The drawing 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); } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 93acd88fb14..bbb772a4cea 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -329,10 +329,33 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e InvalidateMeasure(); } + private Thickness _layoutThickness = default; + + private Thickness LayoutThickness + { + get + { + if (_layoutThickness == default) + { + var borderThickness = BorderThickness; + + if (UseLayoutRounding) + { + var scale = LayoutHelper.GetLayoutScale(this); + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); + } + + _layoutThickness = borderThickness; + } + + return _layoutThickness; + } + } + /// 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); } @@ -400,13 +423,22 @@ internal Size ArrangeOverrideImpl(Size finalSize, Vector offset) { 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 verticalContentAlignment = VerticalContentAlignment; - var useLayoutRounding = UseLayoutRounding; var availableSize = finalSize; var sizeForChild = availableSize; - var scale = LayoutHelper.GetLayoutScale(this); var originX = offset.X; var originY = offset.Y; diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index d4154a6d0cd..d24be57d2ba 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -52,16 +52,44 @@ public static Size MeasureChild(ILayoutable? control, Size availableSize, Thickn 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) + { + 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)); 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; + } + /// /// Invalidates measure for given control and all visual children recursively. /// @@ -126,6 +154,32 @@ public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY)); } + /// + /// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen + /// coordinates. + /// + /// Input thickness. + /// DPI along x-dimension. + /// DPI along y-dimension. + /// Value of thickness 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 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) + ); + } + + + /// /// Calculates the value to be used for layout rounding at high DPI. /// @@ -163,8 +217,7 @@ public static double RoundLayoutValue(double value, double dpiScale) 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 09e0c4263a7..23a76f6ee21 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -643,17 +643,27 @@ protected virtual void ArrangeCore(Rect finalRect) { if (IsVisible) { + var useLayoutRounding = UseLayoutRounding; + var scale = LayoutHelper.GetLayoutScale(this); + var margin = Margin; var originX = finalRect.X + margin.Left; 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( Math.Max(0, finalRect.Width - margin.Left - margin.Right), Math.Max(0, finalRect.Height - margin.Top - margin.Bottom)); var horizontalAlignment = HorizontalAlignment; var verticalAlignment = VerticalAlignment; var size = availableSizeMinusMargins; - var scale = LayoutHelper.GetLayoutScale(this); - var useLayoutRounding = UseLayoutRounding; if (horizontalAlignment != HorizontalAlignment.Stretch) { From a39de875aa00ab18fdd2db74121b7071c9ee2a8b Mon Sep 17 00:00:00 2001 From: AndrejBunjac Date: Mon, 18 Apr 2022 17:34:31 +0200 Subject: [PATCH 2/3] Added invalidation to LayoutThickness property in Border and ContentPresenter and implemented minor review fixes. --- src/Avalonia.Controls/Border.cs | 38 +++++++++++++++---- .../Presenters/ContentPresenter.cs | 29 ++++++++++---- src/Avalonia.Layout/Layoutable.cs | 2 +- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index ce64570dc8c..53de95ac41b 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -1,8 +1,10 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Shapes; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -88,6 +90,18 @@ static Border() AffectsMeasure(BorderThicknessProperty); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + switch (change.Property.Name) + { + case nameof(UseLayoutRounding): + case nameof(BorderThickness): + _layoutThickness = null; + break; + } + } + /// /// Gets or sets a brush with which to paint the background. /// @@ -169,29 +183,39 @@ public BoxShadows BoxShadow set => SetValue(BoxShadowProperty, value); } - private Thickness _layoutThickness = default; + private Thickness? _layoutThickness; + private double _scale; private Thickness LayoutThickness { get { - if (_layoutThickness == default) + VerifyScale(); + + if (_layoutThickness == null) { var borderThickness = BorderThickness; if (UseLayoutRounding) - { - var scale = LayoutHelper.GetLayoutScale(this); - borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); - } + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale); _layoutThickness = borderThickness; } - return _layoutThickness; + return _layoutThickness.Value; } } + private void VerifyScale() + { + var currentScale = LayoutHelper.GetLayoutScale(this); + if (MathUtilities.AreClose(currentScale, _scale)) + return; + + _scale = currentScale; + _layoutThickness = null; + } + /// /// Renders the control. /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index bbb772a4cea..ae08b4a452b 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Utilities; namespace Avalonia.Controls.Presenters { @@ -249,6 +250,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs case nameof(TemplatedParent): TemplatedParentChanged(change); break; + case nameof(UseLayoutRounding): + case nameof(BorderThickness): + _layoutThickness = null; + break; } } @@ -329,29 +334,39 @@ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e InvalidateMeasure(); } - private Thickness _layoutThickness = default; + private Thickness? _layoutThickness; + private double _scale; private Thickness LayoutThickness { get { - if (_layoutThickness == default) + VerifyScale(); + + if (_layoutThickness == null) { var borderThickness = BorderThickness; if (UseLayoutRounding) - { - var scale = LayoutHelper.GetLayoutScale(this); - borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, scale, scale); - } + borderThickness = LayoutHelper.RoundLayoutThickness(BorderThickness, _scale, _scale); _layoutThickness = borderThickness; } - return _layoutThickness; + return _layoutThickness.Value; } } + private void VerifyScale() + { + var currentScale = LayoutHelper.GetLayoutScale(this); + if (MathUtilities.AreClose(currentScale, _scale)) + return; + + _scale = currentScale; + _layoutThickness = null; + } + /// public override void Render(DrawingContext context) { diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 23a76f6ee21..df7aa937a06 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -653,7 +653,7 @@ protected virtual void ArrangeCore(Rect finalRect) // 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) + if (useLayoutRounding) { margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale); } From 982e2d5db090305b3f57fc46e226da26839061fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 21 Apr 2022 16:15:36 +0200 Subject: [PATCH 3/3] Fix merge error due to changed API. And moved fields to live with other field. --- src/Avalonia.Controls/Border.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 53de95ac41b..06368eb5c6b 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -71,6 +71,8 @@ public partial class Border : Decorator, IVisualWithRoundRectClip AvaloniaProperty.Register(nameof(BorderLineJoin), PenLineJoin.Miter); private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); + private Thickness? _layoutThickness; + private double _scale; /// /// Initializes static members of the class. @@ -90,7 +92,7 @@ static Border() AffectsMeasure(BorderThicknessProperty); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) @@ -183,9 +185,6 @@ public BoxShadows BoxShadow set => SetValue(BoxShadowProperty, value); } - private Thickness? _layoutThickness; - private double _scale; - private Thickness LayoutThickness { get