From 253354fe2432318e3b0ad5b15e7eb460eeaca7b4 Mon Sep 17 00:00:00 2001 From: Yoyo Date: Wed, 12 Aug 2026 21:03:35 +0800 Subject: [PATCH] Fix WrapPanel layout jitter with LineSpacing (#21942) Co-authored-by: Julien Lebosquain --- src/Avalonia.Controls/WrapPanel.cs | 13 ++++- .../WrapPanelTests.cs | 52 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index ec5bdd57c3..2ebfd2f001 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -228,6 +228,7 @@ namespace Avalonia.Controls bool itemExists = false; bool lineExists = false; var itemsAlignment = ItemsAlignment; + var useLayoutRounding = UseLayoutRounding; // If we have infinite space on the U axis, we always use Start alignment to avoid strange behavior if (uvConstraint.U is double.PositiveInfinity) itemsAlignment = WrapPanelItemsAlignment.Start; @@ -250,7 +251,7 @@ namespace Avalonia.Controls itemHeightSet ? itemHeight : child.DesiredSize.Height); var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0; - if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line + if (GreaterThan(useLayoutRounding, curLineSize.U + childSize.U + nextSpacing, uvConstraint.U)) // Need to switch to another line { panelSize.U = Max(curLineSize.U, panelSize.U); panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0); @@ -299,6 +300,7 @@ namespace Avalonia.Controls bool itemExists = false; bool lineExists = false; var itemsAlignment = ItemsAlignment; + var useLayoutRounding = UseLayoutRounding; // If we have infinite space on the U axis, we always use Start alignment to avoid strange behavior if (uvFinalSize.U is double.PositiveInfinity) itemsAlignment = WrapPanelItemsAlignment.Start; @@ -311,7 +313,7 @@ namespace Avalonia.Controls itemHeightSet ? itemHeight : child.DesiredSize.Height); var nextSpacing = itemExists && child.IsVisible ? itemSpacing : 0; - if (MathUtilities.GreaterThan(curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line + if (GreaterThan(useLayoutRounding, curLineSize.U + childSize.U + nextSpacing, uvFinalSize.U)) // Need to switch to another line { accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first ArrangeLine(curLineSize.V, firstInLine, i); @@ -408,6 +410,13 @@ namespace Avalonia.Controls } } + private static bool GreaterThan(bool useLayoutRounding, double value1, double value2) + { + return useLayoutRounding + ? value1 > value2 && value1 - value2 > LayoutHelper.LayoutEpsilon + : MathUtilities.GreaterThan(value1, value2); + } + private struct UVSize { internal UVSize(Orientation orientation, double width, double height) diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index 0e76bfc73e..020d6f61d4 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -261,6 +261,56 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 140, 30, 50), target.Children[3].Bounds); } + [Theory] + [InlineData(true, 50)] + [InlineData(false, 110)] + public void Measure_Respects_Layout_Rounding_At_Wrap_Boundary(bool useLayoutRounding, double expectedHeight) + { + var target = new WrapPanel + { + UseLayoutRounding = useLayoutRounding, + ItemSpacing = 10, + LineSpacing = 10, + Children = + { + new Border { Width = 45, Height = 50 }, + new Border { Width = 45, Height = 50 }, + } + }; + + target.Measure(new Size(100 - LayoutHelper.LayoutEpsilon / 2, double.PositiveInfinity)); + + Assert.Equal(expectedHeight, target.DesiredSize.Height); + } + + [Theory] + [InlineData(true, 0)] + [InlineData(false, 60)] + public void Arrange_Respects_Layout_Rounding_At_Wrap_Boundary(bool useLayoutRounding, double expectedSecondChildY) + { + var target = new WrapPanel + { + UseLayoutRounding = useLayoutRounding, + ItemSpacing = 10, + LineSpacing = 10, + Children = + { + new Border { Width = 45, Height = 50, UseLayoutRounding = false }, + new Border + { + Width = 45 + LayoutHelper.LayoutEpsilon / 2, + Height = 50, + UseLayoutRounding = false, + }, + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 110)); + + Assert.Equal(expectedSecondChildY, target.Children[1].Bounds.Y); + } + [Fact] public void Lays_Out_Horizontally_On_Separate_Lines_With_Spacing_Invisible() { @@ -350,7 +400,7 @@ namespace Avalonia.Controls.UnitTests new Border // line 1 { Width = 50, - Height = 50 + Height = 50 }, } };