Files
Ursa.Avalonia/src/Ursa/Controls/ElasticWrapPanel.cs
2024-06-14 22:04:07 +08:00

481 lines
16 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Utilities;
using static System.Math;
namespace Ursa.Controls;
public class ElasticWrapPanel : WrapPanel
{
static ElasticWrapPanel()
{
IsFillHorizontalProperty.Changed.AddClassHandler<Control>(OnIsFillPropertyChanged);
IsFillVerticalProperty.Changed.AddClassHandler<Control>(OnIsFillPropertyChanged);
AffectsMeasure<ElasticWrapPanel>(IsFillHorizontalProperty, IsFillVerticalProperty);
}
#region AttachedProperty
public static void SetFixToRB(Control element, bool value)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.SetValue(FixToRBProperty, value);
}
public static bool GetIsFixToRB(Control element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
return element.GetValue(FixToRBProperty);
}
/// <summary>
/// Fixed to [Right (Horizontal Mode) | Bottom (Vertical Mode)]
/// which will cause line breaks
/// </summary>
public static readonly AttachedProperty<bool> FixToRBProperty =
AvaloniaProperty.RegisterAttached<ElasticWrapPanel, Control, bool>("FixToRB");
#endregion
#region StyledProperty
public bool IsFillHorizontal
{
get => GetValue(IsFillHorizontalProperty);
set => SetValue(IsFillHorizontalProperty, value);
}
public static readonly StyledProperty<bool> IsFillHorizontalProperty =
AvaloniaProperty.Register<ElasticWrapPanel, bool>(nameof(IsFillHorizontal));
public bool IsFillVertical
{
get => GetValue(IsFillVerticalProperty);
set => SetValue(IsFillVerticalProperty, value);
}
public static readonly StyledProperty<bool> IsFillVerticalProperty =
AvaloniaProperty.Register<ElasticWrapPanel, bool>(nameof(IsFillVertical));
private static void OnIsFillPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
(d as ElasticWrapPanel)?.InvalidateMeasure();
}
#endregion
protected override Size MeasureOverride(Size constraint)
{
double itemWidth = ItemWidth;
double itemHeight = ItemHeight;
var orientation = Orientation;
var children = Children;
// Determine the space required for items in the same row/column based on horizontal/vertical arrangement
var curLineSize = new UVSize(orientation);
// Calculate the total space requirement for this ElasticWrapPanel
var panelSize = new UVSize(orientation);
// Measure UVSize with the given space constraint, used for measuring elements when ItemWidth and ItemHeight are not set
var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
bool itemWidthSet = !double.IsNaN(itemWidth);
bool itemHeightSet = !double.IsNaN(itemHeight);
var childConstraint = new Size(
itemWidthSet ? itemWidth : constraint.Width,
itemHeightSet ? itemHeight : constraint.Height);
// Measurement space for elements with FixToRB=True
Size childFixConstraint = new Size(constraint.Width, constraint.Height);
switch (orientation)
{
case Orientation.Horizontal when itemHeightSet:
childFixConstraint = new Size(constraint.Width, itemHeight);
break;
case Orientation.Vertical when itemWidthSet:
childFixConstraint = new Size(itemWidth, constraint.Height);
break;
}
// This is the size for non-space measurement
UVSize itemSetSize = new UVSize(orientation,
itemWidthSet ? itemWidth : 0,
itemHeightSet ? itemHeight : 0);
foreach (var child in children)
{
UVSize sz;
if (GetIsFixToRB(child))
{
// Measure the element when it needs to be fixed to the right/bottom
child.Measure(childFixConstraint);
sz = new UVSize(orientation, child.DesiredSize.Width, child.DesiredSize.Height);
// Ensure the width/height is within the constraint limits
if (sz.U > 0 && itemSetSize.U > 0)
{
if (sz.U < itemSetSize.U)
{
sz.U = itemSetSize.U;
}
else
{
sz.U = Min(sz.U, uvConstraint.U);
}
}
if (sz.V > 0 && itemSetSize.V > 0 && sz.V < itemSetSize.V)
{
sz.V = itemSetSize.V;
}
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U))
{
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
curLineSize = sz;
}
else
{
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
}
curLineSize = new UVSize(orientation);
}
else
{
// Flow passes its own constraint to children
child.Measure(childConstraint);
// This is the size of the child in UV space
sz = new UVSize(orientation,
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) // Need to switch to another line
{
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) // The element is wider than the constraint - give it a separate line
{
panelSize.U = Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(orientation);
}
}
else // Continue to accumulate a line
{
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
}
}
}
// The last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
// Go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
bool itemWidthSet = !double.IsNaN(ItemWidth);
bool itemHeightSet = !double.IsNaN(ItemHeight);
// This is the size for non-space measurement
UVSize itemSetSize = new UVSize(Orientation,
itemWidthSet ? ItemWidth : 0,
itemHeightSet ? ItemHeight : 0);
// Measure UVSize with the given space constraint, used for measuring elements when ItemWidth and ItemHeight are not set
UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
// Collection of elements in the same direction (row/column)
List<UVCollection> lineUVCollection = new List<UVCollection>();
#region Get the collection of elements in the same direction
// Current collection of elements in a row/column
UVCollection curLineUIs = new UVCollection(Orientation, itemSetSize);
// Iterate over the child elements
var children = Children;
foreach (var child in children)
{
UVSize sz;
if (GetIsFixToRB(child))
{
// Measure the element when it needs to be fixed to the right/bottom
sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
double lengthCount = 1;
if (sz.U > 0 && itemSetSize.U > 0)
{
if (sz.U < itemSetSize.U)
{
sz.U = itemSetSize.U;
}
else
{
lengthCount = Ceiling(sz.U / itemSetSize.U);
sz.U = Min(sz.U, uvFinalSize.U);
}
}
if (sz.V > 0 && itemSetSize.V > 0 && sz.V < itemSetSize.V)
{
sz.V = itemSetSize.V;
}
if (MathUtilities.GreaterThan(curLineUIs.TotalU + sz.U, uvFinalSize.U))
{
if (curLineUIs.Count > 0)
{
lineUVCollection.Add(curLineUIs);
}
curLineUIs = new UVCollection(Orientation, itemSetSize);
curLineUIs.Add(child, sz, Convert.ToInt32(lengthCount));
}
else
{
curLineUIs.Add(child, sz, Convert.ToInt32(lengthCount));
}
lineUVCollection.Add(curLineUIs);
curLineUIs = new UVCollection(Orientation, itemSetSize);
}
else
{
sz = new UVSize(Orientation,
itemWidthSet ? ItemWidth : child.DesiredSize.Width,
itemHeightSet ? ItemHeight : child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineUIs.TotalU + sz.U, uvFinalSize.U)) // Need to switch to another line
{
if (curLineUIs.Count > 0)
{
lineUVCollection.Add(curLineUIs);
}
curLineUIs = new UVCollection(Orientation, itemSetSize);
curLineUIs.Add(child, sz);
if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U))
{
lineUVCollection.Add(curLineUIs);
curLineUIs = new UVCollection(Orientation, itemSetSize);
}
}
else
{
curLineUIs.Add(child, sz);
}
}
}
if (curLineUIs.Count > 0 && !lineUVCollection.Contains(curLineUIs))
{
lineUVCollection.Add(curLineUIs);
}
#endregion
bool isFillU = false;
bool isFillV = false;
switch (Orientation)
{
case Orientation.Horizontal:
isFillU = IsFillHorizontal;
isFillV = IsFillVertical;
break;
case Orientation.Vertical:
isFillU = IsFillVertical;
isFillV = IsFillHorizontal;
break;
}
if (lineUVCollection.Count > 0)
{
double accumulatedV = 0;
double adaptULength = 0;
bool isAdaptV = false;
double adaptVLength = 0;
if (isFillU)
{
if (itemSetSize.U > 0)
{
int maxElementCount = lineUVCollection
.Max(uiSet => uiSet.UICollection
.Sum(p => p.Value.ULengthCount));
adaptULength = (uvFinalSize.U - maxElementCount * itemSetSize.U) / maxElementCount;
adaptULength = Max(adaptULength, 0);
}
}
if (isFillV)
{
if (itemSetSize.V > 0)
{
isAdaptV = true;
adaptVLength = uvFinalSize.V / lineUVCollection.Count;
}
}
bool isHorizontal = Orientation == Orientation.Horizontal;
foreach (var uvCollection in lineUVCollection)
{
double u = 0;
var lineUIEles = uvCollection.UICollection.Keys.ToList();
double linevV = isAdaptV ? adaptVLength : uvCollection.LineV;
foreach (var child in lineUIEles)
{
UVLengthSize childSize = uvCollection.UICollection[child];
double layoutSlotU = childSize.UVSize.U + childSize.ULengthCount * adaptULength;
double layoutSlotV = isAdaptV ? linevV : childSize.UVSize.V;
if (ElasticWrapPanel.GetIsFixToRB(child) == false)
{
child.Arrange(new Rect(
isHorizontal ? u : accumulatedV,
isHorizontal ? accumulatedV : u,
isHorizontal ? layoutSlotU : layoutSlotV,
isHorizontal ? layoutSlotV : layoutSlotU));
}
else
{
if (itemSetSize.U > 0)
{
layoutSlotU = childSize.ULengthCount * itemSetSize.U +
childSize.ULengthCount * adaptULength;
double leaveULength = uvFinalSize.U - u;
layoutSlotU = Min(leaveULength, layoutSlotU);
}
child.Arrange(new Rect(
isHorizontal ? Max(0, uvFinalSize.U - layoutSlotU) : accumulatedV,
isHorizontal ? accumulatedV : Max(0, uvFinalSize.U - layoutSlotU),
isHorizontal ? layoutSlotU : layoutSlotV,
isHorizontal ? layoutSlotV : layoutSlotU));
}
u += layoutSlotU;
}
accumulatedV += linevV;
lineUIEles.Clear();
}
}
lineUVCollection.ForEach(col => col.Dispose());
lineUVCollection.Clear();
return finalSize;
}
#region Protected Methods
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 { return _orientation == Orientation.Horizontal ? U : V; }
set
{
if (_orientation == Orientation.Horizontal) U = value;
else V = value;
}
}
internal double Height
{
get { return _orientation == Orientation.Horizontal ? V : U; }
set
{
if (_orientation == Orientation.Horizontal) V = value;
else U = value;
}
}
}
private class UVLengthSize
{
public UVSize UVSize { get; set; }
public int ULengthCount { get; set; }
public UVLengthSize(UVSize uvSize, int uLengthCount)
{
this.UVSize = uvSize;
this.ULengthCount = uLengthCount;
}
}
/// <summary>
/// Elements used to store the same row/column
/// </summary>
private class UVCollection : IDisposable
{
public Dictionary<Control, UVLengthSize> UICollection { get; }
private UVSize LineDesireUVSize;
private UVSize ItemSetSize;
public UVCollection(Orientation orientation, UVSize itemSetSize)
{
UICollection = new Dictionary<Control, UVLengthSize>();
LineDesireUVSize = new UVSize(orientation);
ItemSetSize = itemSetSize;
}
public double TotalU => LineDesireUVSize.U;
public double LineV => LineDesireUVSize.V;
public void Add(Control element, UVSize childSize, int itemULength = 1)
{
if (UICollection.ContainsKey(element))
throw new InvalidOperationException("The element already exists and cannot be added repeatedly.");
UICollection[element] = new UVLengthSize(childSize, itemULength);
LineDesireUVSize.U += childSize.U;
LineDesireUVSize.V = Max(LineDesireUVSize.V, childSize.V);
}
public int Count => UICollection.Count;
public void Dispose()
{
UICollection.Clear();
}
}
#endregion
}