feat(flex-panel): 实现FlexPanel及相关基础类型

This commit is contained in:
TuskerManshu
2025-12-24 17:43:48 +08:00
parent 525f26ffbf
commit 954bef1663
5 changed files with 1078 additions and 0 deletions
@@ -0,0 +1,296 @@
using System;
using Avalonia;
using Avalonia.Layout;
namespace AtomUI.Desktop.Controls;
public static class Flex
{
/// <summary>
/// Defines an attached property to control the cross-axis alignment of a specific child in a flex layout.
/// </summary>
public static readonly AttachedProperty<AlignItems?> AlignSelfProperty =
AvaloniaProperty.RegisterAttached<Layoutable, AlignItems?>("AlignSelf", typeof(Flex));
/// <summary>
/// Defines an attached property to control the order of a specific child in a flex layout.
/// </summary>
public static readonly AttachedProperty<int> OrderProperty =
AvaloniaProperty.RegisterAttached<Layoutable, int>("Order", typeof(Flex));
/// <summary>
/// Defines an attached property to control the initial main-axis size of a specific child in a flex layout.
/// </summary>
public static readonly AttachedProperty<FlexBasis> BasisProperty =
AvaloniaProperty.RegisterAttached<Layoutable, FlexBasis>("Basis", typeof(Flex), FlexBasis.Auto);
/// <summary>
/// Defines an attached property to control the factor by which a specific child can shrink
/// along the main-axis in a flex layout.
/// </summary>
public static readonly AttachedProperty<double> ShrinkProperty =
AvaloniaProperty.RegisterAttached<Layoutable, double>("Shrink", typeof(Flex), 1.0,
validate: v => v >= 0.0);
/// <summary>
/// Defines an attached property to control the factor by which a specific child can grow
/// along the main-axis in a flex layout.
/// </summary>
public static readonly AttachedProperty<double> GrowProperty =
AvaloniaProperty.RegisterAttached<Layoutable, double>("Grow", typeof(Flex), 0.0,
validate: v => v >= 0.0);
internal static readonly AttachedProperty<double> BaseLengthProperty =
AvaloniaProperty.RegisterAttached<Layoutable, double>("BaseLength", typeof(Flex), 0.0);
internal static readonly AttachedProperty<double> MinLengthProperty =
AvaloniaProperty.RegisterAttached<Layoutable, double>("MinLength", typeof(Flex), 0.0);
internal static readonly AttachedProperty<double> CurrentLengthProperty =
AvaloniaProperty.RegisterAttached<Layoutable, double>("CurrentLength", typeof(Flex), 0.0);
/// <summary>
/// Gets the cross-axis alignment override for a child item in a <see cref="FlexPanel"/>
/// </summary>
/// <remarks>
/// This property is used to override the <see cref="FlexPanel.AlignItems"/> property for a specific child.
/// When omitted, <see cref="FlexPanel.AlignItems"/> in not overridden.
/// Equivalent to CSS align-self property.
/// </remarks>
public static AlignItems? GetAlignSelf(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(AlignSelfProperty);
}
/// <summary>
/// Sets the cross-axis alignment override for a child item in a <see cref="FlexPanel"/>
/// </summary>
/// <remarks>
/// This property is used to override the <see cref="FlexPanel.AlignItems"/> property for a specific child.
/// When omitted, <see cref="FlexPanel.AlignItems"/> in not overridden.
/// Equivalent to CSS align-self property.
/// </remarks>
public static void SetAlignSelf(Layoutable layoutable, AlignItems? value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(AlignSelfProperty, value);
}
/// <summary>
/// Retrieves the order in which a child item appears within the <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// A lower order value means the item will be positioned earlier within the container.
/// Items with the same order value are laid out in their source document order.
/// When omitted, it is set to 0.
/// Equivalent to CSS order property.
/// </remarks>
public static int GetOrder(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(OrderProperty);
}
/// <summary>
/// Sets the order in which a child item appears within the <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// A lower order value means the item will be positioned earlier within the container.
/// Items with the same order value are laid out in their source document order.
/// When omitted, it is set to 0.
/// Equivalent to CSS order property.
/// </remarks>
public static void SetOrder(Layoutable layoutable, int value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(OrderProperty, value);
}
/// <summary>
/// Gets the initial size along the main-axis of an item in a <see cref="FlexPanel"/>,
/// before free space is distributed according to the flex factors.
/// </summary>
/// <remarks>
/// Either automatic size, a fixed length, or a percentage of the container's size.
/// When omitted, it is set to <see cref="FlexBasis.Auto"/>.
/// Equivalent to CSS flex-basis property.
/// </remarks>
public static FlexBasis GetBasis(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(BasisProperty);
}
/// <summary>
/// Sets the initial size along the main-axis of an item in a <see cref="FlexPanel"/>,
/// before free space is distributed according to the flex factors.
/// </summary>
/// <remarks>
/// Either automatic size, a fixed length, or a percentage of the container's size.
/// When omitted, it is set to <see cref="FlexBasis.Auto"/>.
/// Equivalent to CSS flex-basis property.
/// </remarks>
public static void SetBasis(Layoutable layoutable, FlexBasis value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(BasisProperty, value);
}
/// <summary>
/// Gets the factor by which an item can shrink along the main-axis,
/// relative to other items in a <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// When omitted, it is set to 1.
/// Equivalent to CSS flex-shrink property.
/// </remarks>
public static double GetShrink(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(ShrinkProperty);
}
/// <summary>
/// Sets the factor by which an item can shrink along the main-axis,
/// relative to other items in a <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// When omitted, it is set to 1.
/// Equivalent to CSS flex-shrink property.
/// </remarks>
public static void SetShrink(Layoutable layoutable, double value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(ShrinkProperty, value);
}
/// <summary>
/// Gets the factor by which an item can grow along the main-axis,
/// relative to other items in a <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// When omitted, it is set to 0.
/// Equivalent to CSS flex-grow property.
/// </remarks>
public static double GetGrow(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(GrowProperty);
}
/// <summary>
/// Sets the factor by which an item can grow along the main-axis,
/// relative to other items in a <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// When omitted, it is set to 0.
/// Equivalent to CSS flex-grow property.
/// </remarks>
public static void SetGrow(Layoutable layoutable, double value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(GrowProperty, value);
}
internal static double GetBaseLength(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(BaseLengthProperty);
}
internal static void SetBaseLength(Layoutable layoutable, double value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(BaseLengthProperty, value);
}
internal static double GetMinLength(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(MinLengthProperty);
}
internal static void SetMinLength(Layoutable layoutable, double value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(MinLengthProperty, value);
}
internal static double GetCurrentLength(Layoutable layoutable)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
return layoutable.GetValue(CurrentLengthProperty);
}
internal static void SetCurrentLength(Layoutable layoutable, double value)
{
if (layoutable is null)
{
throw new ArgumentNullException(nameof(layoutable));
}
layoutable.SetValue(CurrentLengthProperty, value);
}
}
@@ -0,0 +1,75 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace AtomUI.Desktop.Controls;
public readonly struct FlexBasis : IEquatable<FlexBasis>
{
public double Value { get; }
public FlexBasisKind Kind { get; }
public FlexBasis(double value, FlexBasisKind kind)
{
if (value < 0 || double.IsNaN(value) || double.IsInfinity(value))
{
throw new ArgumentException($"Invalid basis value: {value}", nameof(value));
}
if (kind < FlexBasisKind.Auto || kind > FlexBasisKind.Relative)
{
throw new ArgumentException($"Invalid basis kind: {kind}", nameof(kind));
}
Value = value;
Kind = kind;
}
public FlexBasis(double value) : this(value, FlexBasisKind.Absolute) { }
public static FlexBasis Auto => new(0.0, FlexBasisKind.Auto);
public bool IsAuto => Kind == FlexBasisKind.Auto;
public bool IsAbsolute => Kind == FlexBasisKind.Absolute;
public bool IsRelative => Kind == FlexBasisKind.Relative;
[SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
public bool Equals(FlexBasis other) =>
(IsAuto && other.IsAuto) || (Value == other.Value && Kind == other.Kind);
public override bool Equals(object? obj) =>
obj is FlexBasis other && Equals(other);
public override int GetHashCode() =>
(Value, Kind).GetHashCode();
public static bool operator ==(FlexBasis left, FlexBasis right) =>
left.Equals(right);
public static bool operator !=(FlexBasis left, FlexBasis right) =>
!left.Equals(right);
public override string ToString()
{
return Kind switch
{
FlexBasisKind.Auto => "Auto",
FlexBasisKind.Absolute => FormattableString.Invariant($"{Value:G17}"),
FlexBasisKind.Relative => FormattableString.Invariant($"{Value * 100:G17}%"),
_ => throw new InvalidOperationException(),
};
}
public static FlexBasis Parse(string str)
{
return str.ToUpperInvariant() switch
{
"AUTO" => Auto,
var s when s.EndsWith("%") => new FlexBasis(ParseDouble(s.TrimEnd('%').TrimEnd()) / 100,
FlexBasisKind.Relative),
_ => new FlexBasis(ParseDouble(str), FlexBasisKind.Absolute),
};
double ParseDouble(string s) => double.Parse(s, CultureInfo.InvariantCulture);
}
}
@@ -0,0 +1,8 @@
namespace AtomUI.Desktop.Controls;
public enum FlexBasisKind
{
Auto,
Absolute,
Relative,
}
@@ -0,0 +1,662 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
namespace AtomUI.Desktop.Controls;
/// <summary>
/// A panel that arranges child controls using CSS FlexBox principles.
/// It organizes child items in one or more lines along a main-axis (either row or column)
/// and provides advanced control over their sizing and layout.
/// </summary>
/// <remarks>
/// See CSS FlexBox specification: https://www.w3.org/TR/css-flexbox-1
/// </remarks>
public sealed class FlexPanel : Panel
{
private static readonly Func<Layoutable, int> s_getOrder = x => x is { } y ? Flex.GetOrder(y) : 0;
private static readonly Func<Layoutable, bool> s_isVisible = x => x.IsVisible;
/// <summary>
/// Defines the <see cref="Direction"/> property.
/// </summary>
public static readonly StyledProperty<FlexDirection> DirectionProperty =
AvaloniaProperty.Register<FlexPanel, FlexDirection>(nameof(Direction));
/// <summary>
/// Defines the <see cref="JustifyContent"/> property.
/// </summary>
public static readonly StyledProperty<JustifyContent> JustifyContentProperty =
AvaloniaProperty.Register<FlexPanel, JustifyContent>(nameof(JustifyContent));
/// <summary>
/// Defines the <see cref="AlignItems"/> property.
/// </summary>
public static readonly StyledProperty<AlignItems> AlignItemsProperty =
AvaloniaProperty.Register<FlexPanel, AlignItems>(nameof(AlignItems), AlignItems.Stretch);
/// <summary>
/// Defines the <see cref="AlignContent"/> property.
/// </summary>
public static readonly StyledProperty<AlignContent> AlignContentProperty =
AvaloniaProperty.Register<FlexPanel, AlignContent>(nameof(AlignContent), AlignContent.Stretch);
/// <summary>
/// Defines the <see cref="Wrap"/> property.
/// </summary>
public static readonly StyledProperty<FlexWrap> WrapProperty =
AvaloniaProperty.Register<FlexPanel, FlexWrap>(nameof(Wrap));
/// <summary>
/// Defines the <see cref="ColumnSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> ColumnSpacingProperty =
AvaloniaProperty.Register<FlexPanel, double>(nameof(ColumnSpacing));
/// <summary>
/// Defines the <see cref="RowSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> RowSpacingProperty =
AvaloniaProperty.Register<FlexPanel, double>(nameof(RowSpacing));
private FlexLayoutState? _state;
static FlexPanel()
{
AffectsMeasure<FlexPanel>(
DirectionProperty,
JustifyContentProperty,
WrapProperty,
ColumnSpacingProperty,
RowSpacingProperty);
AffectsArrange<FlexPanel>(
AlignItemsProperty,
AlignContentProperty);
AffectsParentMeasure<FlexPanel>(
HorizontalAlignmentProperty,
VerticalAlignmentProperty,
Flex.OrderProperty,
Flex.BasisProperty,
Flex.ShrinkProperty,
Flex.GrowProperty);
AffectsParentArrange<FlexPanel>(
Flex.AlignSelfProperty);
}
/// <summary>
/// Gets or sets the direction of the <see cref="FlexPanel"/>'s main-axis,
/// determining the orientation in which child controls are laid out.
/// </summary>
/// <remarks>
/// When omitted, it is set to <see cref="FlexDirection.Row"/>.
/// Equivalent to CSS flex-direction property
/// </remarks>
public FlexDirection Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
/// <summary>
/// Gets or sets the main-axis alignment of child items inside a line of the <see cref="FlexPanel"/>.
/// Typically used to distribute extra free space leftover after flexible lengths and margins have been resolved.
/// </summary>
/// <remarks>
/// When omitted, it is set to <see cref="JustifyContent.FlexStart"/>.
/// Equivalent to CSS justify-content property.
/// </remarks>
public JustifyContent JustifyContent
{
get => GetValue(JustifyContentProperty);
set => SetValue(JustifyContentProperty, value);
}
/// <summary>
/// Gets or sets the cross-axis alignment of all child items inside a line of the <see cref="FlexPanel"/>.
/// Similar to <see cref="JustifyContent"/>, but in the perpendicular direction.
/// </summary>
/// <remarks>
/// When omitted, it is set to <see cref="AlignItems.Stretch"/>.
/// Equivalent to CSS align-items property.
/// </remarks>
public AlignItems AlignItems
{
get => GetValue(AlignItemsProperty);
set => SetValue(AlignItemsProperty, value);
}
/// <summary>
/// Gets or sets the cross-axis alignment of lines in the <see cref="FlexPanel"/> when there is extra space.
/// Similar to <see cref="AlignItems"/>, but for entire lines.
/// <see cref="FlexPanel.Wrap"/> property set to <see cref="FlexWrap.Wrap"/> mode
/// allows controls to be arranged on multiple lines.
/// </summary>
/// <remarks>
/// When omitted, it is set to <see cref="AlignContent.Stretch"/>.
/// Equivalent to CSS align-content property.
/// </remarks>
public AlignContent AlignContent
{
get => GetValue(AlignContentProperty);
set => SetValue(AlignContentProperty, value);
}
/// <summary>
/// Gets or sets the wrap mode, controlling whether the <see cref="FlexPanel"/> is single-line or multi-line.
/// Additionally, it determines the cross-axis stacking direction for new lines.
/// </summary>
/// <remarks>
/// When omitted, it is set to <see cref="FlexWrap.NoWrap"/>.
/// Equivalent to CSS flex-wrap property.
/// </remarks>
public FlexWrap Wrap
{
get => GetValue(WrapProperty);
set => SetValue(WrapProperty, value);
}
/// <summary>
/// Gets or sets the minimum horizontal spacing between child items or lines,
/// depending on main-axis direction of the <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// When omitted, it is set to 0.
/// Similar to CSS column-gap property.
/// </remarks>
public double ColumnSpacing
{
get => GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
/// <summary>
/// Gets or sets the minimum vertical spacing between child items or lines,
/// depending on main-axis direction of the <see cref="FlexPanel"/>.
/// </summary>
/// <remarks>
/// When omitted, it is set to 0.
/// Similar to CSS row-gap property.
/// </remarks>
public double RowSpacing
{
get => GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
var children = (IReadOnlyList<Layoutable>)Children;
children = children.Where(s_isVisible).OrderBy(s_getOrder).ToArray();
var isColumn = Direction is FlexDirection.Column or FlexDirection.ColumnReverse;
var max = Uv.FromSize(availableSize, isColumn);
var spacing = Uv.FromSize(ColumnSpacing, RowSpacing, isColumn);
LineData lineData = default;
var (childIndex, firstChildIndex, itemIndex) = (0, 0, 0);
var flexLines = new List<FlexLine>();
foreach (var element in children)
{
var size = MeasureChild(element, max, isColumn);
if (Wrap != FlexWrap.NoWrap && lineData.U + size.U + itemIndex * spacing.U > max.U)
{
flexLines.Add(new FlexLine(firstChildIndex, childIndex - 1, lineData));
lineData = default;
firstChildIndex = childIndex;
itemIndex = 0;
}
lineData.U += size.U;
lineData.V = Math.Max(lineData.V, size.V);
lineData.Shrink += Flex.GetShrink(element);
lineData.Grow += Flex.GetGrow(element);
lineData.AutoMargins += GetItemAutoMargins(element, isColumn);
itemIndex++;
childIndex++;
}
if (itemIndex != 0)
{
flexLines.Add(new FlexLine(firstChildIndex, firstChildIndex + itemIndex - 1, lineData));
}
var state = new FlexLayoutState(children, flexLines, Wrap);
var totalSpacingV = (flexLines.Count - 1) * spacing.V;
var panelSizeU = flexLines.Count > 0
? flexLines.Max(flexLine => flexLine.U + (flexLine.Count - 1) * spacing.U)
: 0.0;
// Resizing along main axis using grow and shrink factors can affect cross axis, so remeasure affected items and lines.
foreach (var flexLine in flexLines)
{
var (_, _, _, freeU) = GetLineMeasureU(flexLine, max.U, spacing.U);
var (_, _, flexFreeU) = GetLineMultInfo(flexLine, freeU);
if (flexFreeU != 0.0)
{
var lineItems = state.GetLineItems(flexLine).ToArray();
ResolveFlexibleLengths(lineItems, flexFreeU);
foreach (var element in lineItems)
{
var length = Flex.GetCurrentLength(element);
element.Measure(Uv.ToSize(max.WithU(length), isColumn));
}
flexLine.V = lineItems.Max(i => Uv.FromSize(i.DesiredSize, isColumn).V);
}
}
_state = state;
var totalLineV = flexLines.Sum(l => l.V);
var panelSize = flexLines.Count == 0 ? default : new Uv(panelSizeU, totalLineV + totalSpacingV);
var stretchU = isColumn
? VerticalAlignment == VerticalAlignment.Stretch
: HorizontalAlignment == HorizontalAlignment.Stretch;
var stretchV = isColumn
? HorizontalAlignment == HorizontalAlignment.Stretch
: VerticalAlignment == VerticalAlignment.Stretch;
if (stretchU && !double.IsInfinity(max.U))
{
panelSize = panelSize.WithU(max.U);
}
if (stretchV && !double.IsInfinity(max.V))
{
panelSize = panelSize.WithV(max.V);
}
return Uv.ToSize(panelSize, isColumn);
}
/// <inheritdoc />
protected override Size ArrangeOverride(Size finalSize)
{
var state = _state ?? throw new InvalidOperationException();
var isColumn = Direction is FlexDirection.Column or FlexDirection.ColumnReverse;
var isReverse = Direction is FlexDirection.RowReverse or FlexDirection.ColumnReverse;
var panelSize = Uv.FromSize(finalSize, isColumn);
var spacing = Uv.FromSize(ColumnSpacing, RowSpacing, isColumn);
var linesCount = state.Lines.Count;
var totalLineV = state.Lines.Sum(s => s.V);
var totalSpacingV = (linesCount - 1) * spacing.V;
var totalV = totalLineV + totalSpacingV;
var freeV = panelSize.V - totalV;
var alignContent = DetermineAlignContent(AlignContent, freeV, linesCount);
var (v, spacingV) = GetCrossAxisPosAndSpacing(alignContent, spacing, freeV, linesCount);
var scaleV = alignContent == AlignContent.Stretch && totalLineV != 0
? (panelSize.V - totalSpacingV) / totalLineV
: 1.0;
foreach (var line in state.Lines)
{
var lineV = scaleV * line.V;
var (itemsCount, _, _, freeU) = GetLineMeasureU(line, panelSize.U, spacing.U);
var (_, lineAutoMargins, flexFreeU) = GetLineMultInfo(line, freeU);
var lineItems = state.GetLineItems(line).ToArray();
var remainingFreeU = freeU - flexFreeU;
remainingFreeU += ResolveFlexibleLengths(lineItems, flexFreeU);
var currentFreeU = remainingFreeU;
if (lineAutoMargins != 0 && remainingFreeU != 0.0)
{
foreach (var element in lineItems)
{
var baseLength = Flex.GetCurrentLength(element);
var autoMargins = GetItemAutoMargins(element, isColumn);
if (autoMargins != 0)
{
var length = Math.Max(0.0, baseLength + remainingFreeU * autoMargins / lineAutoMargins);
Flex.SetCurrentLength(element, length);
currentFreeU -= length - baseLength;
}
}
}
remainingFreeU = currentFreeU;
var (u, spacingU) = GetMainAxisPosAndSpacing(JustifyContent, spacing, remainingFreeU, itemsCount);
foreach (var element in lineItems)
{
var size = Uv.FromSize(element.DesiredSize, isColumn).WithU(Flex.GetCurrentLength(element));
var align = Flex.GetAlignSelf(element) ?? AlignItems;
var positionV = align switch
{
AlignItems.FlexStart => v,
AlignItems.FlexEnd => v + lineV - size.V,
AlignItems.Center => v + (lineV - size.V) / 2,
AlignItems.Stretch => v,
_ => throw new InvalidOperationException()
};
size = size.WithV(align == AlignItems.Stretch ? lineV : size.V);
var position = new Uv(isReverse ? panelSize.U - size.U - u : u, positionV);
element.Arrange(new Rect(Uv.ToPoint(position, isColumn), Uv.ToSize(size, isColumn)));
u += size.U + spacingU;
}
v += lineV + spacingV;
}
return finalSize;
}
private static Uv MeasureChild(Layoutable element, Uv max, bool isColumn)
{
var basis = Flex.GetBasis(element);
var flexConstraint = basis.Kind switch
{
FlexBasisKind.Auto => max.U,
FlexBasisKind.Absolute => basis.Value,
FlexBasisKind.Relative => max.U * basis.Value,
_ => throw new InvalidOperationException($"Unsupported FlexBasisKind value: {basis.Kind}")
};
element.Measure(Uv.ToSize(max.WithU(flexConstraint), isColumn));
var size = Uv.FromSize(element.DesiredSize, isColumn);
var minLength = Math.Max(size.U, isColumn ? element.MinHeight : element.MinWidth);
Flex.SetMinLength(element, minLength);
var flexLength = basis.Kind switch
{
FlexBasisKind.Auto => size.U,
FlexBasisKind.Absolute or FlexBasisKind.Relative => Math.Max(size.U, flexConstraint),
_ => throw new InvalidOperationException()
};
size = size.WithU(flexLength);
Flex.SetBaseLength(element, flexLength);
Flex.SetCurrentLength(element, flexLength);
return size;
}
private static AlignContent DetermineAlignContent(AlignContent currentAlignContent, double freeV, int linesCount)
{
// Determine AlignContent based on available space and line count
return currentAlignContent switch
{
AlignContent.Stretch when freeV > 0.0 => AlignContent.Stretch,
AlignContent.SpaceBetween when freeV > 0.0 && linesCount > 1 => AlignContent.SpaceBetween,
AlignContent.SpaceAround when freeV > 0.0 && linesCount > 0 => AlignContent.SpaceAround,
AlignContent.SpaceEvenly when freeV > 0.0 && linesCount > 0 => AlignContent.SpaceEvenly,
AlignContent.Stretch => AlignContent.FlexStart,
AlignContent.SpaceBetween => AlignContent.FlexStart,
AlignContent.SpaceAround => AlignContent.Center,
AlignContent.SpaceEvenly => AlignContent.Center,
AlignContent.FlexStart or AlignContent.Center or AlignContent.FlexEnd => currentAlignContent,
_ => throw new InvalidOperationException($"Unsupported AlignContent value: {currentAlignContent}")
};
}
private static (double v, double spacingV) GetCrossAxisPosAndSpacing(AlignContent alignContent, Uv spacing,
double freeV, int linesCount)
{
return alignContent switch
{
AlignContent.FlexStart => (0.0, spacing.V),
AlignContent.FlexEnd => (freeV, spacing.V),
AlignContent.Center => (freeV / 2, spacing.V),
AlignContent.Stretch => (0.0, spacing.V),
AlignContent.SpaceBetween when linesCount > 1 => (0.0, spacing.V + freeV / (linesCount - 1)),
AlignContent.SpaceBetween => (0.0, spacing.V),
AlignContent.SpaceAround when linesCount > 0 => (freeV / linesCount / 2,
spacing.V + freeV / linesCount),
AlignContent.SpaceAround => (freeV / 2, spacing.V),
AlignContent.SpaceEvenly => (freeV / (linesCount + 1), spacing.V + freeV / (linesCount + 1)),
_ => throw new InvalidOperationException($"Unsupported AlignContent value: {alignContent}")
};
}
private static (double u, double spacingU) GetMainAxisPosAndSpacing(JustifyContent justifyContent, Uv spacing,
double remainingFreeU, int itemsCount)
{
if (remainingFreeU <= 0)
{
return justifyContent switch
{
JustifyContent.FlexEnd => (remainingFreeU, spacing.U),
JustifyContent.Center => (remainingFreeU / 2, spacing.U),
_ => (0.0, spacing.U),
};
}
return justifyContent switch
{
JustifyContent.FlexStart => (0.0, spacing.U),
JustifyContent.FlexEnd => (remainingFreeU, spacing.U),
JustifyContent.Center => (remainingFreeU / 2, spacing.U),
JustifyContent.SpaceBetween when itemsCount > 1 =>
(0.0, spacing.U + remainingFreeU / (itemsCount - 1)),
JustifyContent.SpaceBetween => (0.0, spacing.U),
JustifyContent.SpaceAround when itemsCount > 0 =>
(remainingFreeU / itemsCount / 2, spacing.U + remainingFreeU / itemsCount),
JustifyContent.SpaceAround => (remainingFreeU / 2, spacing.U),
JustifyContent.SpaceEvenly when itemsCount > 0 =>
(remainingFreeU / (itemsCount + 1), spacing.U + remainingFreeU / (itemsCount + 1)),
JustifyContent.SpaceEvenly => (remainingFreeU / 2, spacing.U),
_ => throw new InvalidOperationException($"Unsupported JustifyContent value: {justifyContent}")
};
}
private static (int ItemsCount, double TotalSpacingU, double TotalU, double FreeU) GetLineMeasureU(
FlexLine line, double panelSizeU, double spacingU)
{
var itemsCount = line.Count;
var totalSpacingU = (itemsCount - 1) * spacingU;
var totalU = line.U + totalSpacingU;
var freeU = panelSizeU - totalU;
return (itemsCount, totalSpacingU, totalU, freeU);
}
private static (double LineMult, double LineAutoMargins, double RemainingFreeU) GetLineMultInfo(FlexLine line,
double freeU)
{
var lineMult = freeU switch
{
< 0 => line.Shrink,
> 0 => line.Grow,
_ => 0.0,
};
if (lineMult == 0.0)
{
return (0.0, line.AutoMargins, 0.0);
}
// https://www.w3.org/TR/css-flexbox-1/#remaining-free-space
// Sum of flex factors less than 1 reduces remaining free space to be distributed.
return lineMult is > 0 and < 1
? (lineMult, line.AutoMargins, freeU * lineMult)
: (lineMult, line.AutoMargins, freeU);
}
// Distribute free space across flex items, clamping shrink to measured minimum size.
private static double ResolveFlexibleLengths(IReadOnlyList<Layoutable> items, double flexFreeU)
{
foreach (var element in items)
{
Flex.SetCurrentLength(element, Flex.GetBaseLength(element));
}
if (flexFreeU == 0.0)
{
return 0.0;
}
var isShrink = flexFreeU < 0;
var activeItems = new List<(Layoutable Item, double Mult)>(items.Count);
foreach (var element in items)
{
var mult = isShrink ? Flex.GetShrink(element) : Flex.GetGrow(element);
if (mult > 0.0)
{
activeItems.Add((element, mult));
}
}
if (activeItems.Count == 0)
{
return flexFreeU;
}
var remainingFreeU = flexFreeU;
while (activeItems.Count > 0)
{
var totalMult = activeItems.Sum(item => item.Mult);
if (totalMult == 0.0)
{
break;
}
if (isShrink)
{
var clamped = false;
for (var i = activeItems.Count - 1; i >= 0; i--)
{
var (element, mult) = activeItems[i];
var baseLength = Flex.GetBaseLength(element);
var target = baseLength + remainingFreeU * mult / totalMult;
var minLength = GetItemMinLength(element);
if (target < minLength)
{
Flex.SetCurrentLength(element, minLength);
remainingFreeU -= minLength - baseLength;
activeItems.RemoveAt(i);
clamped = true;
}
}
if (clamped)
{
continue;
}
}
var consumed = 0.0;
foreach (var (element, mult) in activeItems)
{
var baseLength = Flex.GetBaseLength(element);
var target = baseLength + remainingFreeU * mult / totalMult;
var length = Math.Max(0.0, target);
Flex.SetCurrentLength(element, length);
consumed += length - baseLength;
}
remainingFreeU -= consumed;
break;
}
return remainingFreeU;
}
private static double GetItemMinLength(Layoutable element)
{
var minLength = Flex.GetMinLength(element);
return Math.Max(0.0, minLength);
}
private static int GetItemAutoMargins(Layoutable element, bool isColumn)
{
// Avoid consuming free space via alignment-based "auto margins" because many controls
// in AtomUI default to Left/Top alignment, which would neutralize justify spacing.
return 0;
}
private readonly struct FlexLayoutState
{
private readonly IReadOnlyList<Layoutable> _children;
public IReadOnlyList<FlexLine> Lines { get; }
public FlexLayoutState(IReadOnlyList<Layoutable> children, List<FlexLine> lines, FlexWrap wrap)
{
if (wrap == FlexWrap.WrapReverse)
{
lines.Reverse();
}
_children = children;
Lines = lines;
}
public IEnumerable<Layoutable> GetLineItems(FlexLine line)
{
for (var i = line.First; i <= line.Last; i++)
{
yield return _children[i];
}
}
}
private struct LineData
{
public double U { get; set; }
public double V { get; set; }
public double Shrink { get; set; }
public double Grow { get; set; }
public int AutoMargins { get; set; }
}
private class FlexLine
{
public FlexLine(int first, int last, LineData line)
{
First = first;
Last = last;
U = line.U;
V = line.V;
Shrink = line.Shrink;
Grow = line.Grow;
AutoMargins = line.AutoMargins;
}
/// <summary>First item index.</summary>
public int First { get; }
/// <summary>Last item index.</summary>
public int Last { get; }
/// <summary>Sum of main sizes of items.</summary>
public double U { get; }
/// <summary>Max of cross sizes of items.</summary>
public double V { get; set; }
/// <summary>Sum of shrink factors of flexible items.</summary>
public double Shrink { get; }
/// <summary>Sum of grow factors of flexible items.</summary>
public double Grow { get; }
/// <summary>Number of "auto margins" along main axis.</summary>
public int AutoMargins { get; }
/// <summary>Number of items.</summary>
public int Count => Last - First + 1;
}
}
@@ -0,0 +1,37 @@
using Avalonia;
namespace AtomUI.Desktop.Controls;
internal struct Uv
{
public Uv(double u, double v)
{
U = u;
V = v;
}
public double U { get; }
public double V { get; }
public static Uv FromSize(double width, double height, bool swap) =>
new(swap ? height : width, swap ? width : height);
public static Uv FromSize(Size size, bool swap) =>
FromSize(size.Width, size.Height, swap);
public static Point ToPoint(Uv uv, bool swap) =>
new(swap ? uv.V : uv.U, swap ? uv.U : uv.V);
public static Size ToSize(Uv uv, bool swap) =>
new(swap ? uv.V : uv.U, swap ? uv.U : uv.V);
public Uv WithU(double u) =>
new(u, V);
public Uv WithV(double v) =>
new(U, v);
public override string ToString() =>
$"U: {U}, V: {V}";
}