Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix problems with UseLayoutRounding. #8146

Merged
merged 3 commits into from
Jun 27, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions src/Avalonia.Base/Layout/LayoutHelper.cs
Original file line number Diff line number Diff line change
@@ -36,11 +36,28 @@ public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
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 @@ public static double GetLayoutScale(ILayoutable control)

/// <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 @@ public static double GetLayoutScale(ILayoutable control)
/// 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 @@ public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScal
);
}



/// <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 @@ public static double RoundLayoutValue(double value, double dpiScale)

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>
24 changes: 15 additions & 9 deletions src/Avalonia.Base/Layout/Layoutable.cs
Original file line number Diff line number Diff line change
@@ -548,6 +548,14 @@ protected virtual Size MeasureCore(Size availableSize)
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 @@ protected virtual Size MeasureCore(Size availableSize)
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 @@ protected virtual void ArrangeCore(Rect finalRect)

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);
14 changes: 13 additions & 1 deletion src/Avalonia.Base/Point.cs
Original file line number Diff line number Diff line change
@@ -188,7 +188,7 @@ public static Point Parse(string s)
}

/// <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 @@ public bool Equals(Point other)
// 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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was already available for Size but not for Point - it's useful in unit tests if not elsewhere.

{
return MathUtilities.AreClose(_x, other._x) &&
MathUtilities.AreClose(_y, other._y);
}

/// <summary>
/// Checks for equality between a point and an object.
/// </summary>
2 changes: 1 addition & 1 deletion src/Avalonia.Controls.DataGrid/DataGridColumn.cs
Original file line number Diff line number Diff line change
@@ -855,7 +855,7 @@ internal void ComputeLayoutRoundedWidth(double leftEdge)
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
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ public class MenuScrollingVisibilityConverter : IMultiValueConverter

if (visibility == ScrollBarVisibility.Auto)
{
if (extent == viewport)
if (MathUtilities.AreClose(extent, viewport))
{
return false;
}
4 changes: 2 additions & 2 deletions src/Avalonia.Controls/Presenters/ContentPresenter.cs
Original file line number Diff line number Diff line change
@@ -635,8 +635,8 @@ internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)

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)
31 changes: 0 additions & 31 deletions tests/Avalonia.Base.UnitTests/Layout/LayoutableTests.cs
Original file line number Diff line number Diff line change
@@ -173,37 +173,6 @@ public void Attaching_Control_To_Tree_Invalidates_Parent_Measure()
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 changes: 140 additions & 0 deletions tests/Avalonia.Base.UnitTests/Layout/LayoutableTests_LayoutRounding.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading