diff --git a/src/AtomUI.Desktop.Controls/FlexPanel/Flex.cs b/src/AtomUI.Desktop.Controls/FlexPanel/Flex.cs
new file mode 100644
index 000000000..5e3a582f1
--- /dev/null
+++ b/src/AtomUI.Desktop.Controls/FlexPanel/Flex.cs
@@ -0,0 +1,296 @@
+using System;
+
+using Avalonia;
+using Avalonia.Layout;
+
+namespace AtomUI.Desktop.Controls;
+
+public static class Flex
+{
+ ///
+ /// Defines an attached property to control the cross-axis alignment of a specific child in a flex layout.
+ ///
+ public static readonly AttachedProperty AlignSelfProperty =
+ AvaloniaProperty.RegisterAttached("AlignSelf", typeof(Flex));
+
+ ///
+ /// Defines an attached property to control the order of a specific child in a flex layout.
+ ///
+ public static readonly AttachedProperty OrderProperty =
+ AvaloniaProperty.RegisterAttached("Order", typeof(Flex));
+
+ ///
+ /// Defines an attached property to control the initial main-axis size of a specific child in a flex layout.
+ ///
+ public static readonly AttachedProperty BasisProperty =
+ AvaloniaProperty.RegisterAttached("Basis", typeof(Flex), FlexBasis.Auto);
+
+ ///
+ /// Defines an attached property to control the factor by which a specific child can shrink
+ /// along the main-axis in a flex layout.
+ ///
+ public static readonly AttachedProperty ShrinkProperty =
+ AvaloniaProperty.RegisterAttached("Shrink", typeof(Flex), 1.0,
+ validate: v => v >= 0.0);
+
+ ///
+ /// Defines an attached property to control the factor by which a specific child can grow
+ /// along the main-axis in a flex layout.
+ ///
+ public static readonly AttachedProperty GrowProperty =
+ AvaloniaProperty.RegisterAttached("Grow", typeof(Flex), 0.0,
+ validate: v => v >= 0.0);
+
+ internal static readonly AttachedProperty BaseLengthProperty =
+ AvaloniaProperty.RegisterAttached("BaseLength", typeof(Flex), 0.0);
+
+ internal static readonly AttachedProperty MinLengthProperty =
+ AvaloniaProperty.RegisterAttached("MinLength", typeof(Flex), 0.0);
+
+ internal static readonly AttachedProperty CurrentLengthProperty =
+ AvaloniaProperty.RegisterAttached("CurrentLength", typeof(Flex), 0.0);
+
+ ///
+ /// Gets the cross-axis alignment override for a child item in a
+ ///
+ ///
+ /// This property is used to override the property for a specific child.
+ /// When omitted, in not overridden.
+ /// Equivalent to CSS align-self property.
+ ///
+ public static AlignItems? GetAlignSelf(Layoutable layoutable)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ return layoutable.GetValue(AlignSelfProperty);
+ }
+
+ ///
+ /// Sets the cross-axis alignment override for a child item in a
+ ///
+ ///
+ /// This property is used to override the property for a specific child.
+ /// When omitted, in not overridden.
+ /// Equivalent to CSS align-self property.
+ ///
+ public static void SetAlignSelf(Layoutable layoutable, AlignItems? value)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ layoutable.SetValue(AlignSelfProperty, value);
+ }
+
+ ///
+ /// Retrieves the order in which a child item appears within the .
+ ///
+ ///
+ /// 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.
+ ///
+ public static int GetOrder(Layoutable layoutable)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ return layoutable.GetValue(OrderProperty);
+ }
+
+ ///
+ /// Sets the order in which a child item appears within the .
+ ///
+ ///
+ /// 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.
+ ///
+ public static void SetOrder(Layoutable layoutable, int value)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ layoutable.SetValue(OrderProperty, value);
+ }
+
+ ///
+ /// Gets the initial size along the main-axis of an item in a ,
+ /// before free space is distributed according to the flex factors.
+ ///
+ ///
+ /// Either automatic size, a fixed length, or a percentage of the container's size.
+ /// When omitted, it is set to .
+ /// Equivalent to CSS flex-basis property.
+ ///
+ public static FlexBasis GetBasis(Layoutable layoutable)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ return layoutable.GetValue(BasisProperty);
+ }
+
+ ///
+ /// Sets the initial size along the main-axis of an item in a ,
+ /// before free space is distributed according to the flex factors.
+ ///
+ ///
+ /// Either automatic size, a fixed length, or a percentage of the container's size.
+ /// When omitted, it is set to .
+ /// Equivalent to CSS flex-basis property.
+ ///
+ public static void SetBasis(Layoutable layoutable, FlexBasis value)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ layoutable.SetValue(BasisProperty, value);
+ }
+
+ ///
+ /// Gets the factor by which an item can shrink along the main-axis,
+ /// relative to other items in a .
+ ///
+ ///
+ /// When omitted, it is set to 1.
+ /// Equivalent to CSS flex-shrink property.
+ ///
+ public static double GetShrink(Layoutable layoutable)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ return layoutable.GetValue(ShrinkProperty);
+ }
+
+ ///
+ /// Sets the factor by which an item can shrink along the main-axis,
+ /// relative to other items in a .
+ ///
+ ///
+ /// When omitted, it is set to 1.
+ /// Equivalent to CSS flex-shrink property.
+ ///
+ public static void SetShrink(Layoutable layoutable, double value)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ layoutable.SetValue(ShrinkProperty, value);
+ }
+
+ ///
+ /// Gets the factor by which an item can grow along the main-axis,
+ /// relative to other items in a .
+ ///
+ ///
+ /// When omitted, it is set to 0.
+ /// Equivalent to CSS flex-grow property.
+ ///
+ public static double GetGrow(Layoutable layoutable)
+ {
+ if (layoutable is null)
+ {
+ throw new ArgumentNullException(nameof(layoutable));
+ }
+
+ return layoutable.GetValue(GrowProperty);
+ }
+
+ ///
+ /// Sets the factor by which an item can grow along the main-axis,
+ /// relative to other items in a .
+ ///
+ ///
+ /// When omitted, it is set to 0.
+ /// Equivalent to CSS flex-grow property.
+ ///
+ 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);
+ }
+}
diff --git a/src/AtomUI.Desktop.Controls/FlexPanel/FlexBasis.cs b/src/AtomUI.Desktop.Controls/FlexPanel/FlexBasis.cs
new file mode 100644
index 000000000..f28afa736
--- /dev/null
+++ b/src/AtomUI.Desktop.Controls/FlexPanel/FlexBasis.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace AtomUI.Desktop.Controls;
+
+public readonly struct FlexBasis : IEquatable
+{
+ 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);
+ }
+}
diff --git a/src/AtomUI.Desktop.Controls/FlexPanel/FlexBasisKind.cs b/src/AtomUI.Desktop.Controls/FlexPanel/FlexBasisKind.cs
new file mode 100644
index 000000000..6bfac26c8
--- /dev/null
+++ b/src/AtomUI.Desktop.Controls/FlexPanel/FlexBasisKind.cs
@@ -0,0 +1,8 @@
+namespace AtomUI.Desktop.Controls;
+
+public enum FlexBasisKind
+{
+ Auto,
+ Absolute,
+ Relative,
+}
diff --git a/src/AtomUI.Desktop.Controls/FlexPanel/FlexPanel.cs b/src/AtomUI.Desktop.Controls/FlexPanel/FlexPanel.cs
new file mode 100644
index 000000000..a83d3511a
--- /dev/null
+++ b/src/AtomUI.Desktop.Controls/FlexPanel/FlexPanel.cs
@@ -0,0 +1,662 @@
+
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace AtomUI.Desktop.Controls;
+
+///
+/// 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.
+///
+///
+/// See CSS FlexBox specification: https://www.w3.org/TR/css-flexbox-1
+///
+public sealed class FlexPanel : Panel
+{
+ private static readonly Func s_getOrder = x => x is { } y ? Flex.GetOrder(y) : 0;
+ private static readonly Func s_isVisible = x => x.IsVisible;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DirectionProperty =
+ AvaloniaProperty.Register(nameof(Direction));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty JustifyContentProperty =
+ AvaloniaProperty.Register(nameof(JustifyContent));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AlignItemsProperty =
+ AvaloniaProperty.Register(nameof(AlignItems), AlignItems.Stretch);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AlignContentProperty =
+ AvaloniaProperty.Register(nameof(AlignContent), AlignContent.Stretch);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty WrapProperty =
+ AvaloniaProperty.Register(nameof(Wrap));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColumnSpacingProperty =
+ AvaloniaProperty.Register(nameof(ColumnSpacing));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty RowSpacingProperty =
+ AvaloniaProperty.Register(nameof(RowSpacing));
+
+ private FlexLayoutState? _state;
+
+ static FlexPanel()
+ {
+ AffectsMeasure(
+ DirectionProperty,
+ JustifyContentProperty,
+ WrapProperty,
+ ColumnSpacingProperty,
+ RowSpacingProperty);
+
+ AffectsArrange(
+ AlignItemsProperty,
+ AlignContentProperty);
+
+ AffectsParentMeasure(
+ HorizontalAlignmentProperty,
+ VerticalAlignmentProperty,
+ Flex.OrderProperty,
+ Flex.BasisProperty,
+ Flex.ShrinkProperty,
+ Flex.GrowProperty);
+
+ AffectsParentArrange(
+ Flex.AlignSelfProperty);
+ }
+
+ ///
+ /// Gets or sets the direction of the 's main-axis,
+ /// determining the orientation in which child controls are laid out.
+ ///
+ ///
+ /// When omitted, it is set to .
+ /// Equivalent to CSS flex-direction property
+ ///
+ public FlexDirection Direction
+ {
+ get => GetValue(DirectionProperty);
+ set => SetValue(DirectionProperty, value);
+ }
+
+ ///
+ /// Gets or sets the main-axis alignment of child items inside a line of the .
+ /// Typically used to distribute extra free space leftover after flexible lengths and margins have been resolved.
+ ///
+ ///
+ /// When omitted, it is set to .
+ /// Equivalent to CSS justify-content property.
+ ///
+ public JustifyContent JustifyContent
+ {
+ get => GetValue(JustifyContentProperty);
+ set => SetValue(JustifyContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the cross-axis alignment of all child items inside a line of the .
+ /// Similar to , but in the perpendicular direction.
+ ///
+ ///
+ /// When omitted, it is set to .
+ /// Equivalent to CSS align-items property.
+ ///
+ public AlignItems AlignItems
+ {
+ get => GetValue(AlignItemsProperty);
+ set => SetValue(AlignItemsProperty, value);
+ }
+
+ ///
+ /// Gets or sets the cross-axis alignment of lines in the when there is extra space.
+ /// Similar to , but for entire lines.
+ /// property set to mode
+ /// allows controls to be arranged on multiple lines.
+ ///
+ ///
+ /// When omitted, it is set to .
+ /// Equivalent to CSS align-content property.
+ ///
+ public AlignContent AlignContent
+ {
+ get => GetValue(AlignContentProperty);
+ set => SetValue(AlignContentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the wrap mode, controlling whether the is single-line or multi-line.
+ /// Additionally, it determines the cross-axis stacking direction for new lines.
+ ///
+ ///
+ /// When omitted, it is set to .
+ /// Equivalent to CSS flex-wrap property.
+ ///
+ public FlexWrap Wrap
+ {
+ get => GetValue(WrapProperty);
+ set => SetValue(WrapProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum horizontal spacing between child items or lines,
+ /// depending on main-axis direction of the .
+ ///
+ ///
+ /// When omitted, it is set to 0.
+ /// Similar to CSS column-gap property.
+ ///
+ public double ColumnSpacing
+ {
+ get => GetValue(ColumnSpacingProperty);
+ set => SetValue(ColumnSpacingProperty, value);
+ }
+
+ ///
+ /// Gets or sets the minimum vertical spacing between child items or lines,
+ /// depending on main-axis direction of the .
+ ///
+ ///
+ /// When omitted, it is set to 0.
+ /// Similar to CSS row-gap property.
+ ///
+ public double RowSpacing
+ {
+ get => GetValue(RowSpacingProperty);
+ set => SetValue(RowSpacingProperty, value);
+ }
+
+ ///
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var children = (IReadOnlyList)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();
+
+ 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);
+ }
+
+ ///
+ 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 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 _children;
+
+ public IReadOnlyList Lines { get; }
+
+ public FlexLayoutState(IReadOnlyList children, List lines, FlexWrap wrap)
+ {
+ if (wrap == FlexWrap.WrapReverse)
+ {
+ lines.Reverse();
+ }
+ _children = children;
+ Lines = lines;
+ }
+
+ public IEnumerable 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;
+ }
+
+ /// First item index.
+ public int First { get; }
+
+ /// Last item index.
+ public int Last { get; }
+
+ /// Sum of main sizes of items.
+ public double U { get; }
+
+ /// Max of cross sizes of items.
+ public double V { get; set; }
+
+ /// Sum of shrink factors of flexible items.
+ public double Shrink { get; }
+
+ /// Sum of grow factors of flexible items.
+ public double Grow { get; }
+
+ /// Number of "auto margins" along main axis.
+ public int AutoMargins { get; }
+
+ /// Number of items.
+ public int Count => Last - First + 1;
+ }
+}
diff --git a/src/AtomUI.Desktop.Controls/FlexPanel/Uv.cs b/src/AtomUI.Desktop.Controls/FlexPanel/Uv.cs
new file mode 100644
index 000000000..11459e80d
--- /dev/null
+++ b/src/AtomUI.Desktop.Controls/FlexPanel/Uv.cs
@@ -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}";
+}