185 lines
5.5 KiB
C#
185 lines
5.5 KiB
C#
using System.Numerics;
|
|
using Voile.Rendering;
|
|
|
|
namespace Voile.UI.Containers;
|
|
|
|
public enum FlexDirection
|
|
{
|
|
Row,
|
|
Column
|
|
}
|
|
|
|
public enum JustifyContent
|
|
{
|
|
/// <summary>
|
|
/// Align children to the start of the line.
|
|
/// </summary>
|
|
Start,
|
|
/// <summary>
|
|
/// Align children to the center of the line.
|
|
/// </summary>
|
|
Center,
|
|
/// <summary>
|
|
/// Align children to the end of the line.
|
|
/// </summary>
|
|
End,
|
|
/// <summary>
|
|
/// Equal space between items (no space at edges if multiple).
|
|
/// </summary>
|
|
SpaceBetween
|
|
}
|
|
|
|
public enum AlignItems
|
|
{
|
|
/// <summary>
|
|
/// Align to the top or left (cross-axis)
|
|
/// </summary>
|
|
Start,
|
|
/// <summary>
|
|
/// Center along the cross-axis.
|
|
/// </summary>
|
|
Center,
|
|
/// <summary>
|
|
/// Align to the bottom or right.
|
|
/// </summary>
|
|
End
|
|
}
|
|
|
|
/// <summary>
|
|
/// A flexible layout container that arranges its child elements in rows or columns, similar to CSS flexbox. Supports wrapping, alignment, spacing, and space distribution.
|
|
/// </summary>
|
|
public class FlexContainer : Container
|
|
{
|
|
/// <summary>
|
|
/// Layout direction to use with this <see cref="FlexContainer"/>.
|
|
/// </summary>
|
|
public FlexDirection Direction { get; set; } = FlexDirection.Row;
|
|
/// <summary>
|
|
/// Controls main-axis alignment of child elements. By default, children will be positioned at the start of a line.
|
|
/// </summary>
|
|
public JustifyContent Justify { get; set; } = JustifyContent.Start;
|
|
/// <summary>
|
|
/// Controls cross-axis alignment (start, center, end).
|
|
/// </summary>
|
|
public AlignItems Align { get; set; } = AlignItems.Start;
|
|
/// <summary>
|
|
/// If true, children wrap onto multiple lines (rows or columns).
|
|
/// </summary>
|
|
public bool Wrap { get; set; } = false;
|
|
/// <summary>
|
|
/// Space between child elements.
|
|
/// </summary>
|
|
public float Gap { get; set; } = 16f;
|
|
|
|
/// <summary>
|
|
/// (Reserved) If true, distributes extra space among children (not implemented)
|
|
/// </summary>
|
|
public bool DistributeRemainingSpace { get; set; } = false;
|
|
|
|
public FlexContainer() : base() { }
|
|
|
|
public FlexContainer(Rect minimumSize, List<UIElement> children) : base(minimumSize, children) { }
|
|
|
|
public override void Arrange()
|
|
{
|
|
float containerMainSize = (Direction == FlexDirection.Row) ? Size.Width : Size.Height;
|
|
float mainPos = 0.0f;
|
|
float crossPos = 0.0f;
|
|
|
|
List<List<UIElement>> lines = new();
|
|
List<UIElement> currentLine = new();
|
|
float lineMainSum = 0f;
|
|
float maxCross = 0f;
|
|
|
|
foreach (var child in Children)
|
|
{
|
|
Vector2 childSize = GetChildSize(child);
|
|
float mainSize = GetMainSize(childSize);
|
|
float crossSize = GetCrossSize(childSize);
|
|
|
|
bool wrapLine = Wrap && currentLine.Count > 0 && (lineMainSum + mainSize + Gap > containerMainSize);
|
|
|
|
if (wrapLine)
|
|
{
|
|
lines.Add(currentLine);
|
|
currentLine = new();
|
|
lineMainSum = 0f;
|
|
maxCross += crossSize + Gap;
|
|
}
|
|
|
|
currentLine.Add(child);
|
|
lineMainSum += mainSize + (currentLine.Count > 1 ? Gap : 0);
|
|
}
|
|
|
|
if (currentLine.Count > 0)
|
|
{
|
|
lines.Add(currentLine);
|
|
}
|
|
|
|
float currentCross = crossPos;
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
float lineMainLength = line.Sum(child => GetMainSize(GetChildSize(child))) + Gap * (line.Count - 1);
|
|
float justifyOffset = GetJustifyOffset(containerMainSize, lineMainLength, line.Count);
|
|
|
|
float currentMain = mainPos + justifyOffset;
|
|
float maxLineCross = line.Max(child => GetCrossSize(GetChildSize(child)));
|
|
|
|
foreach (var child in line)
|
|
{
|
|
Vector2 childSize = GetChildSize(child);
|
|
|
|
float alignedCross = currentCross + GetAlignOffset(maxLineCross, GetCrossSize(childSize));
|
|
|
|
Vector2 childPos = (Direction == FlexDirection.Row)
|
|
? new Vector2(currentMain, alignedCross)
|
|
: new Vector2(alignedCross, currentMain);
|
|
|
|
child.LocalPosition = childPos;
|
|
|
|
currentMain += GetMainSize(childSize) + Gap;
|
|
}
|
|
|
|
currentCross += maxLineCross + Gap;
|
|
}
|
|
}
|
|
|
|
private Vector2 GetChildSize(IElement child)
|
|
{
|
|
if (child is IResizeableElement resizeable)
|
|
return new Vector2(resizeable.MinimumSize.Width, resizeable.MinimumSize.Height);
|
|
return new Vector2(child.Size.Width, child.Size.Height);
|
|
}
|
|
|
|
private float GetMainSize(Vector2 size)
|
|
{
|
|
return Direction == FlexDirection.Row ? size.X : size.Y;
|
|
}
|
|
|
|
private float GetCrossSize(Vector2 size)
|
|
{
|
|
return Direction == FlexDirection.Row ? size.Y : size.X;
|
|
}
|
|
|
|
private float GetJustifyOffset(float containerSize, float totalMain, int itemCount)
|
|
{
|
|
return Justify switch
|
|
{
|
|
JustifyContent.Center => (containerSize - totalMain) / 2f,
|
|
JustifyContent.End => containerSize - totalMain,
|
|
JustifyContent.SpaceBetween => (itemCount > 1) ? 0 : (containerSize - totalMain) / 2f,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
private float GetAlignOffset(float maxCross, float childCross)
|
|
{
|
|
return Align switch
|
|
{
|
|
AlignItems.Center => (maxCross - childCross) / 2f,
|
|
AlignItems.End => maxCross - childCross,
|
|
_ => 0f
|
|
};
|
|
}
|
|
} |