diff --git a/samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs b/samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs
new file mode 100644
index 0000000000..c5c3e0e2ed
--- /dev/null
+++ b/samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+
+namespace ControlCatalog.Converter
+{
+ internal sealed class FlexDemoNumberToThicknessConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is int x && targetType.IsAssignableFrom(typeof(Thickness)))
+ {
+ var y = 16 + 2 * ((x * 5) % 9);
+ return new Thickness(2 * y, y);
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
+ throw new NotSupportedException();
+ }
+}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index adea1b90fc..ca7f4ba303 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -128,6 +128,9 @@
+
+
+
diff --git a/samples/ControlCatalog/Pages/FlexPage.axaml b/samples/ControlCatalog/Pages/FlexPage.axaml
new file mode 100644
index 0000000000..0da7a86c9e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/FlexPage.axaml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/FlexPage.axaml.cs b/samples/ControlCatalog/Pages/FlexPage.axaml.cs
new file mode 100644
index 0000000000..0d6e452d69
--- /dev/null
+++ b/samples/ControlCatalog/Pages/FlexPage.axaml.cs
@@ -0,0 +1,37 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public partial class FlexPage : UserControl
+ {
+ public FlexPage()
+ {
+ InitializeComponent();
+
+ DataContext = new FlexViewModel();
+ }
+
+ private void OnItemTapped(object? sender, RoutedEventArgs e)
+ {
+ if (sender is ListBoxItem control && DataContext is FlexViewModel vm && control.DataContext is FlexItemViewModel item)
+ {
+ if (vm.SelectedItem != null)
+ {
+ vm.SelectedItem.IsSelected = false;
+ }
+
+ if (vm.SelectedItem == item)
+ {
+ vm.SelectedItem = null;
+ }
+ else
+ {
+ vm.SelectedItem = item;
+ vm.SelectedItem.IsSelected = true;
+ }
+ }
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/FlexItemViewModel.cs b/samples/ControlCatalog/ViewModels/FlexItemViewModel.cs
new file mode 100644
index 0000000000..5f9dc19d24
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/FlexItemViewModel.cs
@@ -0,0 +1,118 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public sealed class FlexItemViewModel : ViewModelBase
+ {
+ internal const AlignItems AlignSelfAuto = (AlignItems)(-1);
+
+ private AlignItems _alignSelf;
+
+ private bool _isSelected;
+ private bool _isVisible = true;
+
+ private AlignItems _alignSelfItem = AlignSelfAuto;
+ private int _order;
+ private double _shrink = 1.0;
+ private double _grow;
+ private double _basisValue = 100.0;
+ private FlexBasisKind _basisKind;
+ private HorizontalAlignment _horizontalAlignment;
+ private VerticalAlignment _verticalAlignment;
+
+ public FlexItemViewModel(int value)
+ {
+ Value = value;
+
+ _alignSelf = AlignSelfItem == AlignSelfAuto ? default(AlignItems) : AlignSelfItem;
+
+ var color = Random.Shared.Next();
+
+ Color = new SolidColorBrush((uint)color);
+ }
+
+ public int Value { get; }
+
+ public Brush Color { get; }
+
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set => this.RaiseAndSetIfChanged(ref _isSelected, value);
+ }
+
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => this.RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public AlignItems AlignSelfItem
+ {
+ get => _alignSelfItem;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _alignSelfItem, value);
+ this.RaisePropertyChanged(nameof(AlignSelf));
+ }
+ }
+
+ public AlignItems? AlignSelf => _alignSelf;
+
+ public int Order
+ {
+ get => _order;
+ set => this.RaiseAndSetIfChanged(ref _order, value);
+ }
+
+ public double Shrink
+ {
+ get => _shrink;
+ set => this.RaiseAndSetIfChanged(ref _shrink, value);
+ }
+
+ public double Grow
+ {
+ get => _grow;
+ set => this.RaiseAndSetIfChanged(ref _grow, value);
+ }
+
+ public double BasisValue
+ {
+ get => _basisValue;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _basisValue, value);
+ this.RaisePropertyChanged(nameof(Basis));
+ }
+ }
+
+ public FlexBasisKind BasisKind
+ {
+ get => _basisKind;
+ set
+ {
+ this.RaiseAndSetIfChanged(ref _basisKind, value);
+ this.RaisePropertyChanged(nameof(Basis));
+ }
+ }
+
+ public FlexBasis Basis => new(_basisValue, _basisKind);
+
+ public HorizontalAlignment HorizontalAlignment
+ {
+ get => _horizontalAlignment;
+ set => this.RaiseAndSetIfChanged(ref _horizontalAlignment, value);
+ }
+
+ public VerticalAlignment VerticalAlignment
+ {
+ get => _verticalAlignment;
+ set => this.RaiseAndSetIfChanged(ref _verticalAlignment, value);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/FlexViewModel.cs b/samples/ControlCatalog/ViewModels/FlexViewModel.cs
new file mode 100644
index 0000000000..1cdcbf014f
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/FlexViewModel.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows.Input;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public sealed class FlexViewModel : ViewModelBase
+ {
+ private readonly ObservableCollection _numbers;
+
+ private FlexDirection _direction = FlexDirection.Row;
+ private JustifyContent _justifyContent = JustifyContent.FlexStart;
+ private AlignItems _alignItems = AlignItems.FlexStart;
+ private AlignContent _alignContent = AlignContent.FlexStart;
+ private FlexWrap _wrap = FlexWrap.Wrap;
+
+ private int _columnSpacing = 64;
+ private int _rowSpacing = 32;
+
+ private int _currentNumber = 41;
+
+ private FlexItemViewModel? _selectedItem;
+
+ public FlexViewModel()
+ {
+ _numbers = new ObservableCollection(Enumerable.Range(1, 40).Select(x => new FlexItemViewModel(x)));
+
+ Numbers = new ReadOnlyObservableCollection(_numbers);
+
+ AddItemCommand = MiniCommand.Create(AddItem);
+ RemoveItemCommand = MiniCommand.Create(RemoveItem);
+ }
+
+ public IEnumerable DirectionValues { get; } = Enum.GetValues(typeof(FlexDirection));
+
+ public IEnumerable JustifyContentValues { get; } = Enum.GetValues(typeof(JustifyContent));
+
+ public IEnumerable AlignItemsValues { get; } = Enum.GetValues(typeof(AlignItems));
+
+ public IEnumerable AlignContentValues { get; } = Enum.GetValues(typeof(AlignContent));
+
+ public IEnumerable WrapValues { get; } = Enum.GetValues(typeof(FlexWrap));
+
+ public IEnumerable FlexBasisKindValues { get; } = Enum.GetValues(typeof(FlexBasisKind));
+
+ public IEnumerable HorizontalAlignmentValues { get; } = Enum.GetValues(typeof(HorizontalAlignment));
+
+ public IEnumerable VerticalAlignmentValues { get; } = Enum.GetValues(typeof(VerticalAlignment));
+
+ public IEnumerable AlignSelfValues { get; } = Enum.GetValues(typeof(AlignItems)).Cast().Prepend(FlexItemViewModel.AlignSelfAuto);
+
+ public FlexDirection Direction
+ {
+ get => _direction;
+ set => this.RaiseAndSetIfChanged(ref _direction, value);
+ }
+
+ public JustifyContent JustifyContent
+ {
+ get => _justifyContent;
+ set => this.RaiseAndSetIfChanged(ref _justifyContent, value);
+ }
+
+ public AlignItems AlignItems
+ {
+ get => _alignItems;
+ set => this.RaiseAndSetIfChanged(ref _alignItems, value);
+ }
+
+ public AlignContent AlignContent
+ {
+ get => _alignContent;
+ set => this.RaiseAndSetIfChanged(ref _alignContent, value);
+ }
+
+ public FlexWrap Wrap
+ {
+ get => _wrap;
+ set => this.RaiseAndSetIfChanged(ref _wrap, value);
+ }
+
+ public int ColumnSpacing
+ {
+ get => _columnSpacing;
+ set => this.RaiseAndSetIfChanged(ref _columnSpacing, value);
+ }
+
+ public int RowSpacing
+ {
+ get => _rowSpacing;
+ set => this.RaiseAndSetIfChanged(ref _rowSpacing, value);
+ }
+
+ public ReadOnlyObservableCollection Numbers { get; }
+
+ public FlexItemViewModel? SelectedItem
+ {
+ get => _selectedItem;
+ set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
+ }
+
+ public ICommand AddItemCommand { get; }
+
+ public ICommand RemoveItemCommand { get; }
+
+ private void AddItem() => _numbers.Add(new FlexItemViewModel(_currentNumber++));
+
+ private void RemoveItem()
+ {
+ if (SelectedItem is null)
+ {
+ return;
+ }
+
+ _numbers.Remove(SelectedItem);
+
+ SelectedItem.IsSelected = false;
+ SelectedItem = null;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 6e3f40892a..668610cae1 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/src/Avalonia.Controls/FlexPanel/AlignContent.cs b/src/Avalonia.Controls/FlexPanel/AlignContent.cs
new file mode 100644
index 0000000000..b1d079e478
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/AlignContent.cs
@@ -0,0 +1,46 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines the alignment mode of the lines inside a along the cross-axis.
+ ///
+ public enum AlignContent
+ {
+ ///
+ /// Lines are packed toward the start of the container.
+ ///
+ FlexStart,
+
+ ///
+ /// Lines are packed toward the end of the container.
+ ///
+ FlexEnd,
+
+ ///
+ /// Lines are packed toward the center of the container
+ ///
+ Center,
+
+ ///
+ /// Lines are stretched to take up the remaining space.
+ ///
+ ///
+ /// This is the default value.
+ ///
+ Stretch,
+
+ ///
+ /// Lines are evenly distributed in the container, with no space on either end.
+ ///
+ SpaceBetween,
+
+ ///
+ /// Lines are evenly distributed in the container, with half-size spaces on either end.
+ ///
+ SpaceAround,
+
+ ///
+ /// Lines are evenly distributed in the container, with equal-size spaces between each line and on either end.
+ ///
+ SpaceEvenly
+ }
+}
diff --git a/src/Avalonia.Controls/FlexPanel/AlignItems.cs b/src/Avalonia.Controls/FlexPanel/AlignItems.cs
new file mode 100644
index 0000000000..4a387cfd2e
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/AlignItems.cs
@@ -0,0 +1,38 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines the alignment mode along the cross-axis of child items.
+ ///
+ [SuppressMessage("Naming", "CA1717:Only FlagsAttribute enums should have plural names")]
+ public enum AlignItems
+ {
+ ///
+ /// Items are aligned to the cross-axis start margin edge of the line.
+ ///
+ FlexStart,
+
+ ///
+ /// Items are aligned to the cross-axis end margin edge of the line.
+ ///
+ FlexEnd,
+
+ ///
+ /// Items are aligned to the cross-axis center of the line.
+ ///
+ ///
+ /// If the cross size of the line is less than that of the child item,
+ /// it will overflow equally in both directions.
+ ///
+ Center,
+
+ ///
+ /// Items are stretched to fill the cross size of the line.
+ ///
+ ///
+ /// This is the default value.
+ ///
+ Stretch
+ }
+}
diff --git a/src/Avalonia.Controls/FlexPanel/Flex.cs b/src/Avalonia.Controls/FlexPanel/Flex.cs
new file mode 100644
index 0000000000..bf7c19f38a
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/Flex.cs
@@ -0,0 +1,271 @@
+using System;
+
+using Avalonia.Layout;
+
+namespace Avalonia.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 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 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/Avalonia.Controls/FlexPanel/FlexBasis.cs b/src/Avalonia.Controls/FlexPanel/FlexBasis.cs
new file mode 100644
index 0000000000..b25fe9f88e
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/FlexBasis.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Specifies the initial size of a flex item.
+ ///
+ public readonly struct FlexBasis : IEquatable
+ {
+ public double Value { get; }
+
+ public FlexBasisKind Kind { get; }
+
+ ///
+ /// Initializes an instance of and sets the value and
+ ///
+ /// The value of the
+ /// The . This determines how the value affects the size of the flex item/>
+ ///
+ 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;
+ }
+
+ ///
+ /// Initializes an instance of and sets the absolute value
+ ///
+ /// The absolute value of the
+ ///
+ 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(),
+ };
+ }
+
+ ///
+ /// Converts a string flex-basis value to a instance.
+ ///
+ /// The value to parse.
+ ///
+ 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/Avalonia.Controls/FlexPanel/FlexBasisKind.cs b/src/Avalonia.Controls/FlexPanel/FlexBasisKind.cs
new file mode 100644
index 0000000000..b43781d28c
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/FlexBasisKind.cs
@@ -0,0 +1,23 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Determines how affects the size of the flex item
+ ///
+ public enum FlexBasisKind
+ {
+ ///
+ /// Uses the measured Width and Height of the to determine the initial size of the item.
+ ///
+ Auto,
+
+ ///
+ /// The initial size of the item is set to the value.
+ ///
+ Absolute,
+
+ ///
+ /// Indicates the value is a percentage, and the size of the flex item is scaled by it.
+ ///
+ Relative,
+ }
+}
diff --git a/src/Avalonia.Controls/FlexPanel/FlexDirection.cs b/src/Avalonia.Controls/FlexPanel/FlexDirection.cs
new file mode 100644
index 0000000000..ed8a59e4b3
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/FlexDirection.cs
@@ -0,0 +1,31 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Describes the orientation and direction along which items are placed inside the
+ ///
+ public enum FlexDirection
+ {
+ ///
+ /// Items are placed along the horizontal axis, starting from the left
+ ///
+ ///
+ /// This is the default value.
+ ///
+ Row,
+
+ ///
+ /// Items are placed along the horizontal axis, starting from the right
+ ///
+ RowReverse,
+
+ ///
+ /// Items are placed along the vertical axis, starting from the top
+ ///
+ Column,
+
+ ///
+ /// Items are placed along the vertical axis, starting from the bottom
+ ///
+ ColumnReverse
+ }
+}
diff --git a/src/Avalonia.Controls/FlexPanel/FlexPanel.cs b/src/Avalonia.Controls/FlexPanel/FlexPanel.cs
new file mode 100644
index 0000000000..51d75d2b6b
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/FlexPanel.cs
@@ -0,0 +1,582 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Avalonia.Controls;
+using Avalonia.Layout;
+
+namespace Avalonia.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 (itemsCount, totalSpacingU, totalU, freeU) = GetLineMeasureU(flexLine, max.U, spacing.U);
+ var (lineMult, autoMargins, remainingFreeU) = GetLineMultInfo(flexLine, freeU);
+
+ if (lineMult != 0.0 && remainingFreeU != 0.0)
+ {
+ foreach (var element in state.GetLineItems(flexLine))
+ {
+ var baseLength = Flex.GetBaseLength(element);
+ var mult = GetItemMult(element, freeU);
+ if (mult != 0.0)
+ {
+ var length = Math.Max(0.0, baseLength + remainingFreeU * mult / lineMult);
+ element.Measure(Uv.ToSize(max.WithU(length), isColumn));
+ }
+ }
+
+ flexLine.V = state.GetLineItems(flexLine).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);
+ 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, totalSpacingU, totalU, freeU) = GetLineMeasureU(line, panelSize.U, spacing.U);
+ var (lineMult, lineAutoMargins, remainingFreeU) = GetLineMultInfo(line, freeU);
+
+ var currentFreeU = remainingFreeU;
+ if (lineMult != 0.0 && remainingFreeU != 0.0)
+ {
+ foreach (var element in state.GetLineItems(line))
+ {
+ var baseLength = Flex.GetBaseLength(element);
+ var mult = GetItemMult(element, freeU);
+ if (mult != 0.0)
+ {
+ var length = Math.Max(0.0, baseLength + remainingFreeU * mult / lineMult);
+ Flex.SetCurrentLength(element, length);
+ currentFreeU -= length - baseLength;
+ }
+ }
+ }
+ remainingFreeU = currentFreeU;
+
+ if (lineAutoMargins != 0 && remainingFreeU != 0.0)
+ {
+ foreach (var element in state.GetLineItems(line))
+ {
+ 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, line, spacing, remainingFreeU, itemsCount);
+
+ foreach (var element in state.GetLineItems(line))
+ {
+ 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 / 100,
+ _ => throw new InvalidOperationException($"Unsupported FlexBasisKind value: {basis.Kind}")
+ };
+ element.Measure(Uv.ToSize(max.WithU(flexConstraint), isColumn));
+
+ var size = Uv.FromSize(element.DesiredSize, isColumn);
+
+ 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
+ {
+ // If there's free vertical space, handle distribution based on the content alignment
+ 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,
+
+ // Default alignments when there's no free space or not enough lines
+ 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, FlexLine line,
+ Uv spacing, double remainingFreeU, int itemsCount)
+ {
+ return line.Grow > 0 ? (0.0, spacing.U) : 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,
+ };
+ // 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);
+ }
+
+ private static double GetItemMult(Layoutable element, double freeU)
+ {
+ var mult = freeU switch
+ {
+ < 0 => Flex.GetShrink(element),
+ > 0 => Flex.GetGrow(element),
+ _ => 0.0,
+ };
+ return mult;
+ }
+
+ private static int GetItemAutoMargins(Layoutable element, bool isColumn)
+ {
+ return isColumn
+ ? element.VerticalAlignment switch
+ {
+ VerticalAlignment.Stretch => 0,
+ VerticalAlignment.Top or VerticalAlignment.Bottom => 1,
+ VerticalAlignment.Center => 2,
+ _ => throw new InvalidOperationException()
+ }
+ : element.HorizontalAlignment switch
+ {
+ HorizontalAlignment.Stretch => 0,
+ HorizontalAlignment.Left or HorizontalAlignment.Right => 1,
+ HorizontalAlignment.Center => 2,
+ _ => throw new InvalidOperationException()
+ };
+ }
+
+ 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 l)
+ {
+ First = first;
+ Last = last;
+ U = l.U;
+ V = l.V;
+ Shrink = l.Shrink;
+ Grow = l.Grow;
+ AutoMargins = l.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/Avalonia.Controls/FlexPanel/FlexWrap.cs b/src/Avalonia.Controls/FlexPanel/FlexWrap.cs
new file mode 100644
index 0000000000..826d7bf661
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/FlexWrap.cs
@@ -0,0 +1,26 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Describes the wrap behavior of the
+ ///
+ public enum FlexWrap
+ {
+ ///
+ /// The is single line.
+ ///
+ ///
+ /// This is the default value.
+ ///
+ NoWrap,
+
+ ///
+ /// The is multi line.
+ ///
+ Wrap,
+
+ ///
+ /// Same as but new lines are added in the opposite cross-axis direction.
+ ///
+ WrapReverse
+ }
+}
diff --git a/src/Avalonia.Controls/FlexPanel/JustifyContent.cs b/src/Avalonia.Controls/FlexPanel/JustifyContent.cs
new file mode 100644
index 0000000000..06a16c048a
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/JustifyContent.cs
@@ -0,0 +1,53 @@
+namespace Avalonia.Controls
+{
+
+ ///
+ /// Describes the main-axis alignment of items inside a line.
+ ///
+ public enum JustifyContent
+ {
+ ///
+ /// Child items are packed toward the start of the line.
+ ///
+ ///
+ /// This is the default value.
+ ///
+ FlexStart,
+
+ ///
+ /// Child items are packed toward the end of the line.
+ ///
+ FlexEnd,
+
+ ///
+ /// Child items are packed toward the center of the line.
+ ///
+ ///
+ /// If the leftover free-space is negative, the child items will overflow equally in both directions.
+ ///
+ Center,
+
+ ///
+ /// Child items are evenly distributed in the line, with no space on either end.
+ ///
+ ///
+ /// If the leftover free-space is negative or there is only a single child item on the line,
+ /// this value is identical to .
+ ///
+ SpaceBetween,
+
+ ///
+ /// Child items are evenly distributed in the line, with half-size spaces on either end.
+ ///
+ ///
+ /// If the leftover free-space is negative or there is only a single child item on the line,
+ /// this value is identical to .
+ ///
+ SpaceAround,
+
+ ///
+ /// Child items are evenly distributed in the line, with equal-size spaces between each item and on either end.
+ ///
+ SpaceEvenly
+ }
+}
diff --git a/src/Avalonia.Controls/FlexPanel/Uv.cs b/src/Avalonia.Controls/FlexPanel/Uv.cs
new file mode 100644
index 0000000000..a49496dae7
--- /dev/null
+++ b/src/Avalonia.Controls/FlexPanel/Uv.cs
@@ -0,0 +1,36 @@
+namespace Avalonia.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 Uv(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 Point(swap ? uv.V : uv.U, swap ? uv.U : uv.V);
+
+ public static Size ToSize(Uv uv, bool swap) =>
+ new Size(swap ? uv.V : uv.U, swap ? uv.U : uv.V);
+
+ public Uv WithU(double u) =>
+ new Uv(u, V);
+
+ public Uv WithV(double v) =>
+ new Uv(U, v);
+
+ public override string ToString() =>
+ $"U: {U}, V: {V}";
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/FlexPanelTests.cs b/tests/Avalonia.Controls.UnitTests/FlexPanelTests.cs
new file mode 100644
index 0000000000..ecfcc4cec4
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/FlexPanelTests.cs
@@ -0,0 +1,415 @@
+using System;
+using Avalonia.Layout;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class FlexPanelTests : ScopedTestBase
+ {
+ [Fact]
+ public void Lays_Items_In_A_Single_Row()
+ {
+ var target = new FlexPanel()
+ {
+ Width = 200,
+ Children =
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(200, 50), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
+ }
+
+ [Fact]
+ public void Lays_Items_In_A_Single_Column()
+ {
+ var target = new FlexPanel()
+ {
+ Direction = FlexDirection.Column,
+ Height = 120,
+ Children =
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(100, 120), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 50, 100, 50), target.Children[1].Bounds);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Row()
+ {
+ var target = new FlexPanel()
+ {
+ Width = 100,
+ Children =
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ },
+ Wrap = FlexWrap.Wrap
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(100, 100), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 50, 100, 50), target.Children[1].Bounds);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Row_In_Reverse_Wrap()
+ {
+ var target = new FlexPanel()
+ {
+ Width = 100,
+ Children =
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ },
+ Wrap = FlexWrap.WrapReverse
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(100, 100), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 50, 100, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 0, 100, 50), target.Children[1].Bounds);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Column()
+ {
+ var target = new FlexPanel()
+ {
+ Height = 60,
+ Children =
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ },
+ Wrap = FlexWrap.Wrap,
+ Direction = FlexDirection.Column
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(200, 60), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 100, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(100, 0, 100, 50), target.Children[1].Bounds);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Column_In_Reverse_Wrap()
+ {
+ var target = new FlexPanel()
+ {
+ Height = 60,
+ Children =
+ {
+ new Border { Height = 50, Width = 100 },
+ new Border { Height = 50, Width = 100 },
+ },
+ Wrap = FlexWrap.WrapReverse,
+ Direction = FlexDirection.Column
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(200, 60), target.Bounds.Size);
+ Assert.Equal(new Rect(100, 0, 100, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 0, 100, 50), target.Children[1].Bounds);
+ }
+
+ public static TheoryData GetAlignItemsValues()
+ {
+ var data = new TheoryData();
+ foreach (var direction in Enum.GetValues())
+ {
+ foreach (var alignment in Enum.GetValues())
+ {
+ data.Add(direction, alignment);
+ }
+ }
+ return data;
+ }
+
+ public static TheoryData GetJustifyContentValues()
+ {
+ var data = new TheoryData();
+ foreach (var direction in Enum.GetValues())
+ {
+ foreach (var justify in Enum.GetValues())
+ {
+ data.Add(direction, justify);
+ }
+ }
+ return data;
+ }
+
+ [Theory, MemberData(nameof(GetAlignItemsValues))]
+ public void Lays_Out_With_Items_Alignment(FlexDirection direction, AlignItems itemsAlignment)
+ {
+ var target = new FlexPanel()
+ {
+ Width = 200,
+ Height = 200,
+ Direction = direction,
+ AlignItems = itemsAlignment,
+ Children =
+ {
+ new Border { Height = 50, Width = 50 },
+ new Border { Height = 50, Width = 50 },
+ }
+ };
+
+ 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);
+
+ Assert.Equal(direction switch
+ {
+ FlexDirection.Row => new(100, 50),
+ FlexDirection.RowReverse => new(100, 50),
+ FlexDirection.Column => new(50, 100),
+ FlexDirection.ColumnReverse => new(50, 100),
+ _ => throw new NotImplementedException()
+ }, rowBounds.Size);
+
+ Assert.Equal((direction, itemsAlignment) switch
+ {
+ (FlexDirection.Row, AlignItems.FlexStart) => new(0, 0),
+ (FlexDirection.Column, AlignItems.FlexStart) => new(0, 0),
+ (FlexDirection.Row, AlignItems.Center) => new(0, 75),
+ (FlexDirection.Column, AlignItems.Center) => new(75, 0),
+ (FlexDirection.Row, AlignItems.FlexEnd) => new(0, 150),
+ (FlexDirection.Column, AlignItems.FlexEnd) => new(150, 0),
+ (FlexDirection.Row, AlignItems.Stretch) => new(0, 75),
+ (FlexDirection.Column, AlignItems.Stretch) => new(75, 0),
+ (FlexDirection.RowReverse, AlignItems.FlexStart) => new(100, 0),
+ (FlexDirection.ColumnReverse, AlignItems.FlexStart) => new(0, 100),
+ (FlexDirection.RowReverse, AlignItems.Center) => new(100, 75),
+ (FlexDirection.ColumnReverse, AlignItems.Center) => new(75, 100),
+ (FlexDirection.RowReverse, AlignItems.FlexEnd) => new(100, 150),
+ (FlexDirection.ColumnReverse, AlignItems.FlexEnd) => new(150, 100),
+ (FlexDirection.RowReverse, AlignItems.Stretch) => new(100, 75),
+ (FlexDirection.ColumnReverse, AlignItems.Stretch) => new(75, 100),
+ _ => throw new NotImplementedException(),
+ }, rowBounds.Position);
+ }
+
+ [Theory, MemberData(nameof(GetJustifyContentValues))]
+ public void Lays_Out_With_Justify_Content(FlexDirection direction, JustifyContent justify)
+ {
+ var target = new FlexPanel()
+ {
+ Width = 200,
+ Height = 200,
+ Direction = direction,
+ JustifyContent = justify,
+ AlignItems = AlignItems.FlexStart,
+ Children =
+ {
+ new Border { Height = 50, Width = 50 },
+ new Border { Height = 50, Width = 50 },
+ }
+ };
+
+ 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);
+
+ Assert.Equal((direction, justify) switch
+ {
+ (FlexDirection.Row, JustifyContent.FlexStart) => new(0, 0),
+ (FlexDirection.Column, JustifyContent.FlexStart) => new(0, 0),
+ (FlexDirection.Row, JustifyContent.Center) => new(50, 0),
+ (FlexDirection.Column, JustifyContent.Center) => new(0, 50),
+ (FlexDirection.Row, JustifyContent.FlexEnd) => new(100, 0),
+ (FlexDirection.Column, JustifyContent.FlexEnd) => new(0, 100),
+ (FlexDirection.Row, JustifyContent.SpaceAround) => new(25, 0),
+ (FlexDirection.Column, JustifyContent.SpaceAround) => new(0, 25),
+ (FlexDirection.Row, JustifyContent.SpaceBetween) => new(0, 0),
+ (FlexDirection.Column, JustifyContent.SpaceBetween) => new(0, 0),
+ (FlexDirection.Row, JustifyContent.SpaceEvenly) => new(33, 0),
+ (FlexDirection.Column, JustifyContent.SpaceEvenly) => new(0, 33),
+ (FlexDirection.RowReverse, JustifyContent.FlexStart) => new(100, 0),
+ (FlexDirection.ColumnReverse, JustifyContent.FlexStart) => new(0, 100),
+ (FlexDirection.RowReverse, JustifyContent.Center) => new(50, 0),
+ (FlexDirection.ColumnReverse, JustifyContent.Center) => new(0, 50),
+ (FlexDirection.RowReverse, JustifyContent.FlexEnd) => new(0, 0),
+ (FlexDirection.ColumnReverse, JustifyContent.FlexEnd) => new(0, 0),
+ (FlexDirection.RowReverse, JustifyContent.SpaceAround) => new(25, 0),
+ (FlexDirection.ColumnReverse, JustifyContent.SpaceAround) => new(0, 25),
+ (FlexDirection.RowReverse, JustifyContent.SpaceBetween) => new(0, 0),
+ (FlexDirection.ColumnReverse, JustifyContent.SpaceBetween) => new(0, 0),
+ (FlexDirection.RowReverse, JustifyContent.SpaceEvenly) => new(33, 0),
+ (FlexDirection.ColumnReverse, JustifyContent.SpaceEvenly) => new(0, 33),
+ _ => throw new NotImplementedException(),
+ }, rowBounds.Position);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Row_With_Spacing()
+ {
+ var target = new FlexPanel()
+ {
+ Width = 100,
+ ColumnSpacing = 10,
+ RowSpacing = 20,
+ Children =
+ {
+ new Border { Height = 50, Width = 60 }, // line 0
+ new Border { Height = 50, Width = 30 }, // line 0
+ new Border { Height = 50, Width = 70 }, // line 1
+ new Border { Height = 50, Width = 30 }, // line 2
+ },
+ Wrap = FlexWrap.Wrap
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(100, 190), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 60, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(70, 0, 30, 50), target.Children[1].Bounds);
+ Assert.Equal(new Rect(0, 70, 70, 50), target.Children[2].Bounds);
+ Assert.Equal(new Rect(0, 140, 30, 50), target.Children[3].Bounds);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Row_With_Spacing_And_Invisible_Content()
+ {
+ var target = new FlexPanel()
+ {
+ ColumnSpacing = 10,
+ Children =
+ {
+ new Border { Height = 50, Width = 60 }, // line 0
+ new Border { Height = 50, Width = 30 , IsVisible = false }, // line 0
+ new Border { Height = 50, Width = 50 }, // line 0
+ },
+ Wrap = FlexWrap.Wrap
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(120, 50), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 60, 50), target.Children[0].Bounds);
+ Assert.Equal(new Rect(70, 0, 50, 50), target.Children[2].Bounds);
+ }
+
+ [Fact]
+ public void Can_Wrap_Items_Into_Next_Column_With_Spacing()
+ {
+ var target = new FlexPanel()
+ {
+ Height = 100,
+ RowSpacing = 10,
+ ColumnSpacing = 20,
+ Children =
+ {
+ new Border { Width = 50, Height = 60 }, // line 0
+ new Border { Width = 50, Height = 30 }, // line 0
+ new Border { Width = 50, Height = 70 }, // line 1
+ new Border { Width = 50, Height = 30 }, // line 2
+ },
+ Wrap = FlexWrap.Wrap,
+ Direction = FlexDirection.Column
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(190, 100), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 50, 60), target.Children[0].Bounds);
+ Assert.Equal(new Rect(0, 70, 50, 30), target.Children[1].Bounds);
+ Assert.Equal(new Rect(70, 0, 50, 70), target.Children[2].Bounds);
+ Assert.Equal(new Rect(140, 0, 50, 30), target.Children[3].Bounds);
+ }
+
+ [Fact]
+ public void Applies_Absolute_FlexBasis_Properties()
+ {
+ var target = new FlexPanel()
+ {
+ Width = 50,
+ Children =
+ {
+ new Border()
+ {
+ [Flex.BasisProperty] = new FlexBasis(20),
+ Height = 15
+ },
+ new Border()
+ {
+ [Flex.BasisProperty] = new FlexBasis(20),
+ Height = 15
+ }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(50, 15), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 20, 15), target.Children[0].Bounds);
+ Assert.Equal(new Rect(20, 0, 20, 15), target.Children[1].Bounds);
+ }
+
+ [Fact]
+ public void Applies_Relative_FlexBasis_Properties()
+ {
+ var target = new FlexPanel()
+ {
+ Width = 50,
+ Children =
+ {
+ new Border()
+ {
+ [Flex.BasisProperty] = new FlexBasis(50, FlexBasisKind.Relative),
+ Height = 15
+ },
+ new Border()
+ {
+ [Flex.BasisProperty] = new FlexBasis(50, FlexBasisKind.Relative),
+ Height = 15
+ }
+ }
+ };
+
+ target.Measure(Size.Infinity);
+ target.Arrange(new Rect(target.DesiredSize));
+
+ Assert.Equal(new Size(50, 15), target.Bounds.Size);
+ Assert.Equal(new Rect(0, 0, 25, 15), target.Children[0].Bounds);
+ Assert.Equal(new Rect(25, 0, 25, 15), target.Children[1].Bounds);
+ }
+ }
+}