Browse Source

Merge a6e83880e9 into 3068850405

pull/20549/merge
Poker 4 days ago
committed by GitHub
parent
commit
f2b72ffc26
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 87
      src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs
  2. 227
      src/Avalonia.Controls/WrapPanel.cs
  3. 73
      tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

87
src/Avalonia.Controls/Utils/OrientationBasedMeasures.cs

@ -0,0 +1,87 @@
using Avalonia.Layout;
namespace Avalonia.Controls.Utils;
internal interface IOrientationBasedMeasures
{
Orientation ScrollOrientation { get; }
bool IsVertical => ScrollOrientation is Orientation.Vertical;
}
internal static class OrientationBasedMeasuresExt
{
extension(IOrientationBasedMeasures m)
{
/// <summary>
/// The length of non-scrolling direction
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
public double Major(Size size) =>
m.IsVertical ? size.Height : size.Width;
/// <summary>
/// The length of scrolling direction
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
public double Minor(Size size) =>
m.IsVertical ? size.Width : size.Height;
public T Minor<T>(T width, T height) =>
m.IsVertical ? width : height;
public T Major<T>(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);
}
}

227
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,32 @@ namespace Avalonia.Controls
/// Items are laid out so the last one in each column/row touches the bottom/right of the panel.
/// </summary>
End,
/// <summary>
/// Items are laid out with equal spacing between them within each column/row.
/// </summary>
/// <remarks>
/// <see cref="WrapPanel.ItemSpacing"/> is become the minimum spacing between items,
/// </remarks>
Justify,
/// <summary>
/// Items are stretched evenly to fill the entire height/width of each column/row (last column/row excluded).
/// </summary>
/// <remarks>
/// <see cref="WrapPanel.ItemWidth"/> or <see cref="WrapPanel.ItemHeight"/> is become the minimum size of items,
/// </remarks>
Stretch,
/*
/// <summary>
/// Items are stretched evenly to fill the entire height/width of each column/row.
/// </summary>
/// <remarks>
/// <see cref="WrapPanel.ItemWidth"/> or <see cref="WrapPanel.ItemHeight"/> is become the minimum size of items,
/// </remarks>
StretchAll
*/
}
/// <summary>
@ -36,7 +64,7 @@ namespace Avalonia.Controls
/// Subsequent ordering happens sequentially from top to bottom or from right to left,
/// depending on the value of the <see cref="Orientation"/> property.
/// </summary>
public class WrapPanel : Panel, INavigableContainer
public class WrapPanel : Panel, INavigableContainer, IOrientationBasedMeasures
{
/// <summary>
/// Defines the <see cref="ItemSpacing"/> dependency property.
@ -137,6 +165,20 @@ namespace Avalonia.Controls
set => SetValue(ItemHeightProperty, value);
}
private Orientation ScrollOrientation { get; set; } = Orientation.Vertical;
Orientation IOrientationBasedMeasures.ScrollOrientation => ScrollOrientation;
/// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == OrientationProperty)
ScrollOrientation = Orientation is Orientation.Horizontal ?
Orientation.Vertical :
Orientation.Horizontal;
}
/// <summary>
/// Gets the next control in the specified direction.
/// </summary>
@ -183,10 +225,8 @@ namespace Avalonia.Controls
{
return children[index];
}
else
{
return null;
}
return null;
}
/// <inheritdoc/>
@ -196,36 +236,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;
// 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(
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 +276,24 @@ 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
}
}
// 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.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;
}
/// <inheritdoc/>
@ -255,32 +303,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 +339,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 +350,72 @@ 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;
var tempItemsAlignment = itemsAlignment;
if (itemsAlignment is WrapPanelItemsAlignment.Stretch && endExcluded == children.Count)
// Don't stretch the last line
tempItemsAlignment = WrapPanelItemsAlignment.Start;
if (tempItemsAlignment 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 (tempItemsAlignment)
{
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 /*or WrapPanelItemsAlignment.StretchAll*/:
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);
}
}
}

73
tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs

@ -1,4 +1,5 @@
using System;
using System.Numerics;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;
@ -65,42 +66,74 @@ namespace Avalonia.Controls.UnitTests
[Theory, MemberData(nameof(GetItemsAlignmentValues))]
public void Lays_Out_With_Items_Alignment(Orientation orientation, WrapPanelItemsAlignment itemsAlignment)
{
var target = new WrapPanel()
var lineHeight = 50d;
var target = new WrapPanel
{
Width = 200,
Height = 200,
Orientation = orientation,
ItemsAlignment = itemsAlignment,
Children =
{
new Border { Height = 50, Width = 50 },
new Border { Height = 50, Width = 50 },
}
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;
// 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)
{
// 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(orientation switch
Assert.Equal(itemsAlignment switch
{
Orientation.Horizontal => new(100, 50),
Orientation.Vertical => new(50, 100),
_ => throw new NotImplementedException()
}, rowBounds.Size);
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((orientation, itemsAlignment) switch
Assert.Equal(itemsAlignment switch
{
(_, WrapPanelItemsAlignment.Start) => 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(),
}, 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]

Loading…
Cancel
Save