From 985ee53477739450d078de8c34c50ecf1ff106a8 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 27 Jan 2026 18:08:21 +0800 Subject: [PATCH 1/6] add Justify & Stretch enum --- .../Utils/OrientationBasedMeasures.cs | 91 ++++++++ src/Avalonia.Controls/WrapPanel.cs | 209 ++++++++++-------- 2 files changed, 210 insertions(+), 90 deletions(-) create mode 100644 src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs diff --git a/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs new file mode 100644 index 0000000000..45328398b8 --- /dev/null +++ b/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs @@ -0,0 +1,91 @@ +namespace Avalonia.Controls.Utils; + +internal enum ScrollOrientation +{ + Horizontal, + Vertical +} + +internal interface IOrientationBasedMeasures +{ + ScrollOrientation ScrollOrientation { get; } + + bool IsVertical => ScrollOrientation is ScrollOrientation.Vertical; +} + +internal static class OrientationBasedMeasuresExt +{ + extension(IOrientationBasedMeasures m) + { + /// + /// The length of non-scrolling direction + /// + /// + /// + public double Major(Size size) => + m.IsVertical ? size.Height : size.Width; + + /// + /// The length of scrolling direction + /// + /// + /// + public double Minor(Size size) => + m.IsVertical ? size.Width : size.Height; + + public T Minor(T width, T height) => + m.IsVertical ? width : height; + + public T Major(T width, T height) => + m.IsVertical ? height : width; + + public void SetMajor(ref Size size, double value) => + size = m.IsVertical ? size.WithHeight(value) : size.WithWidth(value); + + public void SetMinor(ref Size size, double value) => + size = m.IsVertical ? size.WithWidth(value) : size.WithHeight(value); + + public double MajorSize(Rect rect) => + m.IsVertical ? rect.Height : rect.Width; + + public void SetMajorSize(ref Rect rect, double value) => + rect = m.IsVertical ? rect.WithHeight(value) : rect.WithWidth(value); + + public double MinorSize(Rect rect) => + m.IsVertical ? rect.Width : rect.Height; + + public void SetMinorSize(ref Rect rect, double value) => + rect = m.IsVertical ? rect.WithWidth(value) : rect.WithHeight(value); + + public double MajorStart(Rect rect) => + m.IsVertical ? rect.Y : rect.X; + + public double MajorEnd(Rect rect) => + m.IsVertical ? rect.Bottom : rect.Right; + + public double MinorStart(Rect rect) => + m.IsVertical ? rect.X : rect.Y; + + public void SetMinorStart(ref Rect rect, double value) => + rect = m.IsVertical ? rect.WithX(value) : rect.WithY(value); + + public void SetMajorStart(ref Rect rect, double value) => + rect = m.IsVertical ? rect.WithY(value) : rect.WithX(value); + + public double MinorEnd(Rect rect) => + m.IsVertical ? rect.Right : rect.Bottom; + + public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize) => + m.IsVertical ? + new Rect(minor, major, minorSize, majorSize) : + new Rect(major, minor, majorSize, minorSize); + + public Point MinorMajorPoint(double minor, double major) => + m.IsVertical ? + new Point(minor, major) : new Point(major, minor); + + public Size MinorMajorSize(double minor, double major) => + m.IsVertical ? + new Size(minor, major) : new Size(major, minor); + } +} diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index a08695874e..eee902325b 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -4,6 +4,8 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using System.Diagnostics; +using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Utilities; @@ -28,6 +30,22 @@ namespace Avalonia.Controls /// Items are laid out so the last one in each column/row touches the bottom/right of the panel. /// End, + + /// + /// Items are laid out with equal spacing between them within each column/row. + /// + /// + /// is become the minimum spacing between items, + /// + Justify, + + /// + /// Items are laid out with equal spacing between them within each column/row. + /// + /// + /// or is become the minimum spacing between items, + /// + Stretch } /// @@ -36,7 +54,7 @@ namespace Avalonia.Controls /// Subsequent ordering happens sequentially from top to bottom or from right to left, /// depending on the value of the property. /// - public class WrapPanel : Panel, INavigableContainer + public class WrapPanel : Panel, INavigableContainer, IOrientationBasedMeasures { /// /// Defines the dependency property. @@ -137,6 +155,20 @@ namespace Avalonia.Controls set => SetValue(ItemHeightProperty, value); } + private ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical; + + ScrollOrientation IOrientationBasedMeasures.ScrollOrientation => ScrollOrientation; + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + if (change.Property == OrientationProperty) + ScrollOrientation = Orientation is Orientation.Horizontal ? + ScrollOrientation.Vertical : + ScrollOrientation.Horizontal; + } + /// /// Gets the next control in the specified direction. /// @@ -183,10 +215,8 @@ namespace Avalonia.Controls { return children[index]; } - else - { - return null; - } + + return null; } /// @@ -196,36 +226,39 @@ namespace Avalonia.Controls double itemHeight = ItemHeight; double itemSpacing = ItemSpacing; double lineSpacing = LineSpacing; - var orientation = Orientation; - var children = Children; - var curLineSize = new UVSize(orientation); - var panelSize = new UVSize(orientation); - var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height); + var curLineSize = new Size(); + var panelSize = new Size(); bool itemWidthSet = !double.IsNaN(itemWidth); bool itemHeightSet = !double.IsNaN(itemHeight); bool itemExists = false; bool lineExists = false; + var itemsAlignment = ItemsAlignment; + // If we have infinite space on minor axis, we always use Start alignment to avoid strange behavior + if (this.Minor(constraint) is double.PositiveInfinity) + itemsAlignment = WrapPanelItemsAlignment.Start; + // Stretch/Justify need to measure with full constraint on minor axis + if (itemsAlignment is WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify) + this.SetMinor(ref panelSize, this.Minor(constraint)); var childConstraint = new Size( itemWidthSet ? itemWidth : constraint.Width, itemHeightSet ? itemHeight : constraint.Height); - for (int i = 0, count = children.Count; i < count; ++i) + foreach (var child in Children) { - var child = children[i]; // Flow passes its own constraint to children child.Measure(childConstraint); - // This is the size of the child in UV space - UVSize childSize = new UVSize(orientation, + var childSize = new Size( itemWidthSet ? itemWidth : child.DesiredSize.Width, 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 (MathUtilities.GreaterThan(this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing, this.Minor(constraint))) // Need to switch to another line { - panelSize.U = Max(curLineSize.U, panelSize.U); - panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0); + panelSize = this.MinorMajorSize( + Max(this.Minor(curLineSize), this.Minor(panelSize)), + this.Major(panelSize) + this.Major(curLineSize) + (lineExists ? lineSpacing : 0)); curLineSize = childSize; itemExists = child.IsVisible; @@ -233,19 +266,20 @@ namespace Avalonia.Controls } else // Continue to accumulate a line { - curLineSize.U += childSize.U + nextSpacing; - curLineSize.V = Max(childSize.V, curLineSize.V); + curLineSize = this.MinorMajorSize( + this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing, + Max(this.Major(childSize), this.Major(curLineSize))); itemExists |= child.IsVisible; // keep true } } // The last line size, if any should be added - panelSize.U = Max(curLineSize.U, panelSize.U); - panelSize.V += curLineSize.V + (lineExists ? lineSpacing : 0); + panelSize = this.MinorMajorSize( + Max(this.Minor(curLineSize), this.Minor(panelSize)), + this.Major(panelSize) + this.Major(curLineSize) + (lineExists ? lineSpacing : 0)); - // Go from UV space to W/H space - return new Size(panelSize.Width, panelSize.Height); + return panelSize; } /// @@ -255,32 +289,33 @@ namespace Avalonia.Controls double itemHeight = ItemHeight; double itemSpacing = ItemSpacing; double lineSpacing = LineSpacing; - var orientation = Orientation; - bool isHorizontal = orientation == Orientation.Horizontal; var children = Children; int firstInLine = 0; - double accumulatedV = 0; - double itemU = isHorizontal ? itemWidth : itemHeight; - var curLineSize = new UVSize(orientation); - var uvFinalSize = new UVSize(orientation, finalSize.Width, finalSize.Height); + double accumulatedMajor = 0; + double itemMinor = this.Minor(itemWidth, itemHeight); + var curLineSize = new Size(); bool itemWidthSet = !double.IsNaN(itemWidth); bool itemHeightSet = !double.IsNaN(itemHeight); bool itemExists = false; bool lineExists = false; + var itemsAlignment = ItemsAlignment; + // If we have infinite space on minor axis, we always use Start alignment to avoid strange behavior + if (this.Minor(finalSize) is double.PositiveInfinity) + itemsAlignment = WrapPanelItemsAlignment.Start; for (int i = 0; i < children.Count; ++i) { var child = children[i]; - var childSize = new UVSize(orientation, + var childSize = new Size( itemWidthSet ? itemWidth : child.DesiredSize.Width, 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 (MathUtilities.GreaterThan(this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing, this.Minor(finalSize))) // Need to switch to another line { - accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first - ArrangeLine(curLineSize.V, firstInLine, i); - accumulatedV += curLineSize.V; // add the height of the line just arranged + accumulatedMajor += lineExists ? lineSpacing : 0; // add spacing to arrange line first + ArrangeLine(this.Major(curLineSize), firstInLine, i); + accumulatedMajor += this.Major(curLineSize); // add the height of the line just arranged curLineSize = childSize; firstInLine = i; @@ -290,8 +325,9 @@ namespace Avalonia.Controls } else // Continue to accumulate a line { - curLineSize.U += childSize.U + nextSpacing; - curLineSize.V = Max(childSize.V, curLineSize.V); + curLineSize = this.MinorMajorSize( + this.Minor(curLineSize) + this.Minor(childSize) + nextSpacing, + Max(this.Major(childSize), this.Major(curLineSize))); itemExists |= child.IsVisible; // keep true } @@ -300,75 +336,68 @@ namespace Avalonia.Controls // Arrange the last line, if any if (firstInLine < children.Count) { - accumulatedV += lineExists ? lineSpacing : 0; // add spacing to arrange line first - ArrangeLine(curLineSize.V, firstInLine, children.Count); + accumulatedMajor += lineExists ? lineSpacing : 0; // add spacing to arrange line first + ArrangeLine(this.Major(curLineSize), firstInLine, children.Count); } return finalSize; - void ArrangeLine(double lineV, int start, int end) + void ArrangeLine(double lineMajor, int start, int endExcluded) { - bool useItemU = isHorizontal ? itemWidthSet : itemHeightSet; - double u = 0; - if (ItemsAlignment != WrapPanelItemsAlignment.Start) + bool useItemMinor = this.Minor(itemWidthSet, itemHeightSet); + double minorStart = 0d; + var minorSpacing = itemSpacing; + // Count of spacings between items + var minorSpacingCount = -1; + double totalMinor = 0d; + double minorStretchRatio = 1d; + + if (itemsAlignment is not WrapPanelItemsAlignment.Start) { - double totalU = -itemSpacing; - for (int i = start; i < end; ++i) + for (int i = start; i < endExcluded; ++i) { - totalU += GetChildU(i) + (!children[i].IsVisible ? 0 : itemSpacing); + totalMinor += GetChildMinor(i); + if (children[i].IsVisible) + ++minorSpacingCount; } + } - u = ItemsAlignment switch - { - WrapPanelItemsAlignment.Center => (uvFinalSize.U - totalU) / 2, - WrapPanelItemsAlignment.End => uvFinalSize.U - totalU, - WrapPanelItemsAlignment.Start => 0, - _ => throw new ArgumentOutOfRangeException(nameof(ItemsAlignment), ItemsAlignment, null), - }; + Debug.Assert(this.Minor(finalSize) >= totalMinor + minorSpacing * minorSpacingCount); + + switch (itemsAlignment) + { + case WrapPanelItemsAlignment.Start: + break; + case WrapPanelItemsAlignment.Center: + totalMinor += minorSpacing * minorSpacingCount; + minorStart = (this.Minor(finalSize) - totalMinor) / 2; + break; + case WrapPanelItemsAlignment.End: + totalMinor += minorSpacing * minorSpacingCount; + minorStart = this.Minor(finalSize) - totalMinor; + break; + case WrapPanelItemsAlignment.Justify: + var totalMinorSpacing = this.Minor(finalSize) - totalMinor - 0.01; // small epsilon to avoid rounding issues + if (minorSpacingCount > 0) + minorSpacing = totalMinorSpacing / minorSpacingCount; + break; + case WrapPanelItemsAlignment.Stretch: + var finalMinorWithoutSpacing = this.Minor(finalSize) - minorSpacing * minorSpacingCount - 0.01; // small epsilon to avoid rounding issues + minorStretchRatio = finalMinorWithoutSpacing / totalMinor; + break; + default: + throw new ArgumentOutOfRangeException(nameof(itemsAlignment), itemsAlignment, null); } - for (int i = start; i < end; ++i) + for (int i = start; i < endExcluded; ++i) { - double layoutSlotU = GetChildU(i); - children[i].Arrange(isHorizontal ? new(u, accumulatedV, layoutSlotU, lineV) : new(accumulatedV, u, lineV, layoutSlotU)); - u += layoutSlotU + (!children[i].IsVisible ? 0 : itemSpacing); + double layoutSlotMinor = GetChildMinor(i) * minorStretchRatio; + children[i].Arrange(this.MinorMajorRect(minorStart, accumulatedMajor, layoutSlotMinor, lineMajor)); + minorStart += layoutSlotMinor + (children[i].IsVisible ? minorSpacing : 0); } return; - double GetChildU(int i) => useItemU ? itemU : - isHorizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height; - } - } - - private struct UVSize - { - internal UVSize(Orientation orientation, double width, double height) - { - U = V = 0d; - _orientation = orientation; - Width = width; - Height = height; - } - - internal UVSize(Orientation orientation) - { - U = V = 0d; - _orientation = orientation; - } - - internal double U; - internal double V; - private Orientation _orientation; - - internal double Width - { - get => _orientation == Orientation.Horizontal ? U : V; - set { if (_orientation == Orientation.Horizontal) U = value; else V = value; } - } - internal double Height - { - get => _orientation == Orientation.Horizontal ? V : U; - set { if (_orientation == Orientation.Horizontal) V = value; else U = value; } + double GetChildMinor(int i) => useItemMinor ? itemMinor : this.Minor(children[i].DesiredSize); } } } From 0b1342643c8a208ef5cbdbaf9a6e759ab38d3873 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 27 Jan 2026 18:18:31 +0800 Subject: [PATCH 2/6] fix xmldoc --- src/Avalonia.Controls/WrapPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index eee902325b..6b777c5e4b 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -40,10 +40,10 @@ namespace Avalonia.Controls Justify, /// - /// Items are laid out with equal spacing between them within each column/row. + /// Items are stretched evenly to fill the entire height/width of each column/row. /// /// - /// or is become the minimum spacing between items, + /// or is become the minimum size of items, /// Stretch } From f93ffba31fd227d12d0d7d281343a1b8860af9f3 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 27 Jan 2026 19:33:09 +0800 Subject: [PATCH 3/6] add unittest --- .../WrapPanelTests.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index 530d7a4956..fbebb4c420 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -65,16 +65,18 @@ namespace Avalonia.Controls.UnitTests [Theory, MemberData(nameof(GetItemsAlignmentValues))] public void Lays_Out_With_Items_Alignment(Orientation orientation, WrapPanelItemsAlignment itemsAlignment) { - var target = new WrapPanel() + var target = new WrapPanel { Width = 200, Height = 200, + ItemHeight = 50, + ItemWidth = 50, Orientation = orientation, ItemsAlignment = itemsAlignment, Children = { - new Border { Height = 50, Width = 50 }, - new Border { Height = 50, Width = 50 }, + new Border(), + new Border(), } }; @@ -85,21 +87,23 @@ namespace Avalonia.Controls.UnitTests var rowBounds = target.Children[0].Bounds.Union(target.Children[1].Bounds); - Assert.Equal(orientation switch + Assert.Equal((orientation, itemsAlignment) switch { - Orientation.Horizontal => new(100, 50), - Orientation.Vertical => new(50, 100), - _ => throw new NotImplementedException() + (Orientation.Horizontal, WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify) => new(200, 50), + (Orientation.Vertical, WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify) => new(50, 200), + (Orientation.Horizontal, _) => new(100, 50), + (Orientation.Vertical, _) => new(50, 100), + _ => throw new ArgumentOutOfRangeException(nameof(orientation)) }, rowBounds.Size); Assert.Equal((orientation, itemsAlignment) switch { - (_, WrapPanelItemsAlignment.Start) => new(0, 0), + (_, WrapPanelItemsAlignment.Start or WrapPanelItemsAlignment.Justify or WrapPanelItemsAlignment.Stretch) => new(0, 0), (Orientation.Horizontal, WrapPanelItemsAlignment.Center) => new(50, 0), (Orientation.Vertical, WrapPanelItemsAlignment.Center) => new(0, 50), (Orientation.Horizontal, WrapPanelItemsAlignment.End) => new(100, 0), (Orientation.Vertical, WrapPanelItemsAlignment.End) => new(0, 100), - _ => throw new NotImplementedException(), + _ => throw new ArgumentOutOfRangeException(), }, rowBounds.Position); } From 861668e8ca3b4fd5308646b10224ca9482e4fd3b Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 11 Feb 2026 20:12:13 +0800 Subject: [PATCH 4/6] Modify the behavior of the last line of Stretch --- src/Avalonia.Controls/WrapPanel.cs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 6b777c5e4b..f1f9306b80 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -39,13 +39,23 @@ namespace Avalonia.Controls /// Justify, + /// + /// Items are stretched evenly to fill the entire height/width of each column/row (last column/row excluded). + /// + /// + /// or is become the minimum size of items, + /// + Stretch, + + /* /// /// Items are stretched evenly to fill the entire height/width of each column/row. /// /// /// or is become the minimum size of items, /// - Stretch + StretchAll + */ } /// @@ -236,8 +246,8 @@ namespace Avalonia.Controls // If we have infinite space on minor axis, we always use Start alignment to avoid strange behavior if (this.Minor(constraint) is double.PositiveInfinity) itemsAlignment = WrapPanelItemsAlignment.Start; - // Stretch/Justify need to measure with full constraint on minor axis - if (itemsAlignment is WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify) + // Justify/StretchAll need to measure with full constraint on minor axis + if (itemsAlignment is WrapPanelItemsAlignment.Justify /*or WrapPanelItemsAlignment.StretchAll*/) this.SetMinor(ref panelSize, this.Minor(constraint)); var childConstraint = new Size( @@ -274,6 +284,10 @@ namespace Avalonia.Controls } } + // Stretch need to measure with full constraint on minor axis if multiple lines + if (lineExists && itemsAlignment is WrapPanelItemsAlignment.Stretch) + this.SetMinor(ref panelSize, this.Minor(constraint)); + // The last line size, if any should be added panelSize = this.MinorMajorSize( Max(this.Minor(curLineSize), this.Minor(panelSize)), @@ -351,8 +365,12 @@ namespace Avalonia.Controls var minorSpacingCount = -1; double totalMinor = 0d; double minorStretchRatio = 1d; + var tempItemsAlignment = itemsAlignment; + if (itemsAlignment is WrapPanelItemsAlignment.Stretch && endExcluded == children.Count) + // Don't stretch the last line + tempItemsAlignment = WrapPanelItemsAlignment.Start; - if (itemsAlignment is not WrapPanelItemsAlignment.Start) + if (tempItemsAlignment is not WrapPanelItemsAlignment.Start) { for (int i = start; i < endExcluded; ++i) { @@ -364,7 +382,7 @@ namespace Avalonia.Controls Debug.Assert(this.Minor(finalSize) >= totalMinor + minorSpacing * minorSpacingCount); - switch (itemsAlignment) + switch (tempItemsAlignment) { case WrapPanelItemsAlignment.Start: break; @@ -381,7 +399,7 @@ namespace Avalonia.Controls if (minorSpacingCount > 0) minorSpacing = totalMinorSpacing / minorSpacingCount; break; - case WrapPanelItemsAlignment.Stretch: + case WrapPanelItemsAlignment.Stretch /*or WrapPanelItemsAlignment.StretchAll*/: var finalMinorWithoutSpacing = this.Minor(finalSize) - minorSpacing * minorSpacingCount - 0.01; // small epsilon to avoid rounding issues minorStretchRatio = finalMinorWithoutSpacing / totalMinor; break; From 0f73138779eca74f71455bd207624da84dba9a00 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 11 Feb 2026 20:50:46 +0800 Subject: [PATCH 5/6] add UnitTest --- .../WrapPanelTests.cs | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index fbebb4c420..cbe8b925ae 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using Avalonia.Layout; using Avalonia.UnitTests; using Xunit; @@ -65,46 +66,74 @@ namespace Avalonia.Controls.UnitTests [Theory, MemberData(nameof(GetItemsAlignmentValues))] public void Lays_Out_With_Items_Alignment(Orientation orientation, WrapPanelItemsAlignment itemsAlignment) { + var lineHeight = 50d; var target = new WrapPanel { Width = 200, Height = 200, - ItemHeight = 50, - ItemWidth = 50, Orientation = orientation, ItemsAlignment = itemsAlignment, - Children = - { - new Border(), - new Border(), - } + UseLayoutRounding = false }; + if (orientation is Orientation.Horizontal) + { + target.ItemHeight = lineHeight; + target.Children.Add(new Border { MinWidth = 50 }); + target.Children.Add(new Border { MinWidth = 100 }); + target.Children.Add(new Border { MinWidth = 150 }); + } + else + { + target.ItemWidth = lineHeight; + target.Children.Add(new Border { MinHeight = 50 }); + target.Children.Add(new Border { MinHeight = 100 }); + target.Children.Add(new Border { MinHeight = 150 }); + } + target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); Assert.Equal(new Size(200, 200), target.Bounds.Size); - var rowBounds = target.Children[0].Bounds.Union(target.Children[1].Bounds); + var row1Bounds = target.Children[0].Bounds.Union(target.Children[1].Bounds); + var row2Bounds = target.Children[2].Bounds; - Assert.Equal((orientation, itemsAlignment) switch + // fix layout rounding + row1Bounds = new Rect( + Math.Round(row1Bounds.X), + Math.Round(row1Bounds.Y), + Math.Round(row1Bounds.Width), + Math.Round(row1Bounds.Height)); + + if (orientation is Orientation.Vertical) { - (Orientation.Horizontal, WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify) => new(200, 50), - (Orientation.Vertical, WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify) => new(50, 200), - (Orientation.Horizontal, _) => new(100, 50), - (Orientation.Vertical, _) => new(50, 100), - _ => throw new ArgumentOutOfRangeException(nameof(orientation)) - }, rowBounds.Size); - - Assert.Equal((orientation, itemsAlignment) switch + // X <=> Y, Width <=> Height + var reflectionMatrix = new Matrix4x4( + 0, 1, 0, 0, // X' = Y + 1, 0, 0, 0, // Y' = X + 0, 0, 1, 0, // Z' = Z + 0, 0, 0, 1 // W' = W + ); + row1Bounds = row1Bounds.TransformToAABB(reflectionMatrix); + row2Bounds = row2Bounds.TransformToAABB(reflectionMatrix); + } + + Assert.Equal(itemsAlignment switch + { + WrapPanelItemsAlignment.Stretch or WrapPanelItemsAlignment.Justify /*or WrapPanelItemsAlignment.StretchAll*/ => new(0, 0, 200, lineHeight), + WrapPanelItemsAlignment.Center => new(25, 0, 150, lineHeight), + WrapPanelItemsAlignment.End => new(50, 0, 150, lineHeight), + _ => new(0, 0, 150, lineHeight) + }, row1Bounds); + + Assert.Equal(itemsAlignment switch { - (_, WrapPanelItemsAlignment.Start or WrapPanelItemsAlignment.Justify or WrapPanelItemsAlignment.Stretch) => new(0, 0), - (Orientation.Horizontal, WrapPanelItemsAlignment.Center) => new(50, 0), - (Orientation.Vertical, WrapPanelItemsAlignment.Center) => new(0, 50), - (Orientation.Horizontal, WrapPanelItemsAlignment.End) => new(100, 0), - (Orientation.Vertical, WrapPanelItemsAlignment.End) => new(0, 100), - _ => throw new ArgumentOutOfRangeException(), - }, rowBounds.Position); + // WrapPanelItemsAlignment.StretchAll => new(0, lineHeight, 200, lineHeight), + WrapPanelItemsAlignment.Center => new(25, lineHeight, 150, lineHeight), + WrapPanelItemsAlignment.End => new(50, lineHeight, 150, lineHeight), + _ => new(0, 50, 150, 50) + }, row2Bounds); } [Fact] From a3dcbcea35710af192151fba02a7a04efafa9e17 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 11 Feb 2026 20:55:32 +0800 Subject: [PATCH 6/6] remove ScrollOrientation --- .../Utils/OrientationBasedMeasures.cs | 12 ++++-------- src/Avalonia.Controls/WrapPanel.cs | 8 ++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs b/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs index 45328398b8..6d529d9a59 100644 --- a/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs +++ b/src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs @@ -1,16 +1,12 @@ -namespace Avalonia.Controls.Utils; +using Avalonia.Layout; -internal enum ScrollOrientation -{ - Horizontal, - Vertical -} +namespace Avalonia.Controls.Utils; internal interface IOrientationBasedMeasures { - ScrollOrientation ScrollOrientation { get; } + Orientation ScrollOrientation { get; } - bool IsVertical => ScrollOrientation is ScrollOrientation.Vertical; + bool IsVertical => ScrollOrientation is Orientation.Vertical; } internal static class OrientationBasedMeasuresExt diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index f1f9306b80..7da3479c5f 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -165,9 +165,9 @@ namespace Avalonia.Controls set => SetValue(ItemHeightProperty, value); } - private ScrollOrientation ScrollOrientation { get; set; } = ScrollOrientation.Vertical; + private Orientation ScrollOrientation { get; set; } = Orientation.Vertical; - ScrollOrientation IOrientationBasedMeasures.ScrollOrientation => ScrollOrientation; + Orientation IOrientationBasedMeasures.ScrollOrientation => ScrollOrientation; /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -175,8 +175,8 @@ namespace Avalonia.Controls base.OnPropertyChanged(change); if (change.Property == OrientationProperty) ScrollOrientation = Orientation is Orientation.Horizontal ? - ScrollOrientation.Vertical : - ScrollOrientation.Horizontal; + Orientation.Vertical : + Orientation.Horizontal; } ///