Emmanuel Hansen 5 days ago
committed by GitHub
parent
commit
4a80e5f950
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 24
      samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs
  2. 3
      samples/ControlCatalog/MainView.xaml
  3. 219
      samples/ControlCatalog/Pages/FlexPage.axaml
  4. 37
      samples/ControlCatalog/Pages/FlexPage.axaml.cs
  5. 118
      samples/ControlCatalog/ViewModels/FlexItemViewModel.cs
  6. 126
      samples/ControlCatalog/ViewModels/FlexViewModel.cs
  7. 2
      src/Avalonia.Controls/Avalonia.Controls.csproj
  8. 46
      src/Avalonia.Controls/FlexPanel/AlignContent.cs
  9. 38
      src/Avalonia.Controls/FlexPanel/AlignItems.cs
  10. 271
      src/Avalonia.Controls/FlexPanel/Flex.cs
  11. 90
      src/Avalonia.Controls/FlexPanel/FlexBasis.cs
  12. 23
      src/Avalonia.Controls/FlexPanel/FlexBasisKind.cs
  13. 31
      src/Avalonia.Controls/FlexPanel/FlexDirection.cs
  14. 582
      src/Avalonia.Controls/FlexPanel/FlexPanel.cs
  15. 26
      src/Avalonia.Controls/FlexPanel/FlexWrap.cs
  16. 53
      src/Avalonia.Controls/FlexPanel/JustifyContent.cs
  17. 36
      src/Avalonia.Controls/FlexPanel/Uv.cs
  18. 415
      tests/Avalonia.Controls.UnitTests/FlexPanelTests.cs

24
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();
}
}

3
samples/ControlCatalog/MainView.xaml

@ -128,6 +128,9 @@
<TabItem Header="Flyouts">
<pages:FlyoutsPage />
</TabItem>
<TabItem Header="Flex Panel">
<pages:FlexPage />
</TabItem>
<TabItem Header="Focus">
<pages:FocusPage />
</TabItem>

219
samples/ControlCatalog/Pages/FlexPage.axaml

@ -0,0 +1,219 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:panels="using:Avalonia.Labs.Panels"
xmlns:conv="using:ControlCatalog.Converter"
xmlns:vm="using:ControlCatalog.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="vm:FlexViewModel"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
x:Class="ControlCatalog.Pages.FlexPage">
<UserControl.Resources>
<conv:FlexDemoNumberToThicknessConverter x:Key="NumberToThicknessConverter" />
<DataTemplate x:Key="ItemTemplate"
x:DataType="vm:FlexItemViewModel">
<ListBoxItem Padding="{Binding Value, Converter={StaticResource NumberToThicknessConverter}}"
IsSelected="{Binding IsSelected}"
Background="{Binding Color}"
Gestures.Tapped="OnItemTapped">
<ListBoxItem.Styles>
<Style Selector="ListBoxItem:selected">
<Setter Property="Background" Value="DodgerBlue" />
</Style>
</ListBoxItem.Styles>
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding Value}" />
</ListBoxItem>
</DataTemplate>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="CheckBox">
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="ComboBox">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="RadioButton">
<Setter Property="MinWidth" Value="0" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</UserControl.Styles>
<DockPanel Margin="16">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
Padding="0,0,16,0"
MinWidth="200">
<StackPanel Spacing="16">
<TextBlock FontSize="16"
Text="Properties" />
<!--<panels:FlexPanel ColumnSpacing="16">
<RadioButton IsChecked="{Binding IsItemsControl}"
Content="ItemsControl" />
<RadioButton IsChecked="{Binding IsItemsRepeater}"
Content="ItemsRepeater" />
</panels:FlexPanel>-->
<StackPanel Spacing="8">
<TextBlock Text="Direction:" />
<ComboBox ItemsSource="{Binding DirectionValues}"
SelectedItem="{Binding Direction}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="JustifyContent:" />
<ComboBox ItemsSource="{Binding JustifyContentValues}"
SelectedItem="{Binding JustifyContent}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="AlignItems:" />
<ComboBox ItemsSource="{Binding AlignItemsValues}"
SelectedItem="{Binding AlignItems}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="AlignContent:" />
<ComboBox ItemsSource="{Binding AlignContentValues}"
SelectedItem="{Binding AlignContent}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="Wrap:" />
<ComboBox ItemsSource="{Binding WrapValues}"
SelectedItem="{Binding Wrap}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="ColumnSpacing:" />
<NumericUpDown Minimum="0"
Value="{Binding ColumnSpacing}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="RowSpacing:" />
<NumericUpDown Minimum="0"
Value="{Binding RowSpacing}" />
</StackPanel>
<CheckBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Content="SelectedItem IsVisible"
IsChecked="{Binding SelectedItem.IsVisible}" />
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem AlignSelf:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding AlignSelfValues}"
SelectedItem="{Binding SelectedItem.AlignSelfItem}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Order:" />
<NumericUpDown IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.Order}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Shrink:" />
<NumericUpDown Minimum="0.0"
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.Shrink}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Grow:" />
<NumericUpDown Minimum="0.0"
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.Grow}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem Basis:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding FlexBasisKindValues}"
SelectedItem="{Binding SelectedItem.BasisKind}" />
<NumericUpDown Minimum="0.0"
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
Value="{Binding SelectedItem.BasisValue}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem HorizontalAlignment:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding HorizontalAlignmentValues}"
SelectedItem="{Binding SelectedItem.HorizontalAlignment}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock Text="SelectedItem VerticalAlignment:" />
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}"
ItemsSource="{Binding VerticalAlignmentValues}"
SelectedItem="{Binding SelectedItem.VerticalAlignment}" />
</StackPanel>
<Grid ColumnDefinitions="*,8,*">
<Button Grid.Column="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding AddItemCommand}"
Content="Add Item" />
<Button Grid.Column="2"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding RemoveItemCommand}"
Content="Remove Item" />
</Grid>
</StackPanel>
</ScrollViewer>
<ItemsControl BorderBrush="#777"
BorderThickness="1"
ItemsSource="{Binding Numbers}"
ItemTemplate="{StaticResource ItemTemplate}">
<ItemsControl.Styles>
<Style Selector="ContentPresenter"
x:DataType="vm:FlexItemViewModel">
<Setter Property="Flex.AlignSelf" Value="{Binding AlignSelf}" />
<Setter Property="Flex.Order" Value="{Binding Order}" />
<Setter Property="Flex.Shrink" Value="{Binding Shrink}" />
<Setter Property="Flex.Grow" Value="{Binding Grow}" />
<Setter Property="Flex.Basis" Value="{Binding Basis}" />
<Setter Property="HorizontalAlignment" Value="{Binding HorizontalAlignment}" />
<Setter Property="VerticalAlignment" Value="{Binding VerticalAlignment}" />
<Setter Property="IsVisible" Value="{Binding IsVisible}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<FlexPanel Direction="{Binding Direction}"
JustifyContent="{Binding JustifyContent}"
AlignItems="{Binding AlignItems}"
AlignContent="{Binding AlignContent}"
Wrap="{Binding Wrap}"
ColumnSpacing="{Binding ColumnSpacing}"
RowSpacing="{Binding RowSpacing}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DockPanel>
</UserControl>

37
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;
}
}
}
}
}

118
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);
}
}
}

126
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<FlexItemViewModel> _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<FlexItemViewModel>(Enumerable.Range(1, 40).Select(x => new FlexItemViewModel(x)));
Numbers = new ReadOnlyObservableCollection<FlexItemViewModel>(_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<AlignItems>().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<FlexItemViewModel> 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;
}
}
}

2
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -18,7 +18,7 @@
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)"/>
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="AvaloniaUI.DiagnosticsSupport.Avalonia, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />

46
src/Avalonia.Controls/FlexPanel/AlignContent.cs

@ -0,0 +1,46 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines the alignment mode of the lines inside a <see cref="FlexPanel"/> along the cross-axis.
/// </summary>
public enum AlignContent
{
/// <summary>
/// Lines are packed toward the start of the container.
/// </summary>
FlexStart,
/// <summary>
/// Lines are packed toward the end of the container.
/// </summary>
FlexEnd,
/// <summary>
/// Lines are packed toward the center of the container
/// </summary>
Center,
/// <summary>
/// Lines are stretched to take up the remaining space.
/// </summary>
/// <remarks>
/// This is the default value.
/// </remarks>
Stretch,
/// <summary>
/// Lines are evenly distributed in the container, with no space on either end.
/// </summary>
SpaceBetween,
/// <summary>
/// Lines are evenly distributed in the container, with half-size spaces on either end.
/// </summary>
SpaceAround,
/// <summary>
/// Lines are evenly distributed in the container, with equal-size spaces between each line and on either end.
/// </summary>
SpaceEvenly
}
}

38
src/Avalonia.Controls/FlexPanel/AlignItems.cs

@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the alignment mode along the cross-axis of <see cref="FlexPanel"/> child items.
/// </summary>
[SuppressMessage("Naming", "CA1717:Only FlagsAttribute enums should have plural names")]
public enum AlignItems
{
/// <summary>
/// Items are aligned to the cross-axis start margin edge of the line.
/// </summary>
FlexStart,
/// <summary>
/// Items are aligned to the cross-axis end margin edge of the line.
/// </summary>
FlexEnd,
/// <summary>
/// Items are aligned to the cross-axis center of the line.
/// </summary>
/// <remarks>
/// If the cross size of the line is less than that of the child item,
/// it will overflow equally in both directions.
/// </remarks>
Center,
/// <summary>
/// Items are stretched to fill the cross size of the line.
/// </summary>
/// <remarks>
/// This is the default value.
/// </remarks>
Stretch
}
}

271
src/Avalonia.Controls/FlexPanel/Flex.cs

@ -0,0 +1,271 @@
using System;
using Avalonia.Layout;
namespace Avalonia.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> 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 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);
}
}
}

90
src/Avalonia.Controls/FlexPanel/FlexBasis.cs

@ -0,0 +1,90 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Avalonia.Controls
{
/// <summary>
/// Specifies the initial size of a flex item.
/// </summary>
public readonly struct FlexBasis : IEquatable<FlexBasis>
{
public double Value { get; }
public FlexBasisKind Kind { get; }
/// <summary>
/// Initializes an instance of <see cref="FlexBasis"/> and sets the value and <see cref="FlexBasisKind"/>
/// </summary>
/// <param name="value">The value of the <see cref="FlexBasis"/></param>
/// <param name="kind">The <see cref="FlexBasisKind">. This determines how the value affects the size of the flex item</see>/></param>
/// <exception cref="ArgumentException"></exception>
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;
}
/// <summary>
/// Initializes an instance of <see cref="FlexBasis"/> and sets the absolute value
/// </summary>
/// <param name="value">The absolute value of the <see cref="FlexBasis"/></param>
/// <exception cref="ArgumentException"></exception>
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(),
};
}
/// <summary>
/// Converts a string flex-basis value to a <see cref="FlexBasis"/> instance.
/// </summary>
/// <param name="str">The value to parse.</param>
/// <returns></returns>
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);
}
}
}

23
src/Avalonia.Controls/FlexPanel/FlexBasisKind.cs

@ -0,0 +1,23 @@
namespace Avalonia.Controls
{
/// <summary>
/// Determines how <see cref="FlexBasis"/> affects the size of the flex item
/// </summary>
public enum FlexBasisKind
{
/// <summary>
/// Uses the measured Width and Height of the <see cref="FlexPanel"/> to determine the initial size of the item.
/// </summary>
Auto,
/// <summary>
/// The initial size of the item is set to the <see cref="FlexBasis"/> value.
/// </summary>
Absolute,
/// <summary>
/// Indicates the <see cref="FlexBasis"/> value is a percentage, and the size of the flex item is scaled by it.
/// </summary>
Relative,
}
}

31
src/Avalonia.Controls/FlexPanel/FlexDirection.cs

@ -0,0 +1,31 @@
namespace Avalonia.Controls
{
/// <summary>
/// Describes the orientation and direction along which items are placed inside the <see cref="FlexPanel"/>
/// </summary>
public enum FlexDirection
{
/// <summary>
/// Items are placed along the horizontal axis, starting from the left
/// </summary>
/// <remarks>
/// This is the default value.
/// </remarks>
Row,
/// <summary>
/// Items are placed along the horizontal axis, starting from the right
/// </summary>
RowReverse,
/// <summary>
/// Items are placed along the vertical axis, starting from the top
/// </summary>
Column,
/// <summary>
/// Items are placed along the vertical axis, starting from the bottom
/// </summary>
ColumnReverse
}
}

582
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
{
/// <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 (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);
}
/// <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, 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<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 l)
{
First = first;
Last = last;
U = l.U;
V = l.V;
Shrink = l.Shrink;
Grow = l.Grow;
AutoMargins = l.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;
}
}
}

26
src/Avalonia.Controls/FlexPanel/FlexWrap.cs

@ -0,0 +1,26 @@
namespace Avalonia.Controls
{
/// <summary>
/// Describes the wrap behavior of the <see cref="FlexPanel"/>
/// </summary>
public enum FlexWrap
{
/// <summary>
/// The <see cref="FlexPanel"/> is single line.
/// </summary>
/// <remarks>
/// This is the default value.
/// </remarks>
NoWrap,
/// <summary>
/// The <see cref="FlexPanel"/> is multi line.
/// </summary>
Wrap,
/// <summary>
/// Same as <see cref="Wrap"/> but new lines are added in the opposite cross-axis direction.
/// </summary>
WrapReverse
}
}

53
src/Avalonia.Controls/FlexPanel/JustifyContent.cs

@ -0,0 +1,53 @@
namespace Avalonia.Controls
{
/// <summary>
/// Describes the main-axis alignment of items inside a <see cref="FlexPanel"/> line.
/// </summary>
public enum JustifyContent
{
/// <summary>
/// Child items are packed toward the start of the line.
/// </summary>
/// <remarks>
/// This is the default value.
/// </remarks>
FlexStart,
/// <summary>
/// Child items are packed toward the end of the line.
/// </summary>
FlexEnd,
/// <summary>
/// Child items are packed toward the center of the line.
/// </summary>
/// <remarks>
/// If the leftover free-space is negative, the child items will overflow equally in both directions.
/// </remarks>
Center,
/// <summary>
/// Child items are evenly distributed in the line, with no space on either end.
/// </summary>
/// <remarks>
/// If the leftover free-space is negative or there is only a single child item on the line,
/// this value is identical to <see cref="FlexStart"/>.
/// </remarks>
SpaceBetween,
/// <summary>
/// Child items are evenly distributed in the line, with half-size spaces on either end.
/// </summary>
/// <remarks>
/// If the leftover free-space is negative or there is only a single child item on the line,
/// this value is identical to <see cref="Center"/>.
/// </remarks>
SpaceAround,
/// <summary>
/// Child items are evenly distributed in the line, with equal-size spaces between each item and on either end.
/// </summary>
SpaceEvenly
}
}

36
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}";
}
}

415
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<FlexDirection, AlignItems> GetAlignItemsValues()
{
var data = new TheoryData<FlexDirection, AlignItems>();
foreach (var direction in Enum.GetValues<FlexDirection>())
{
foreach (var alignment in Enum.GetValues<AlignItems>())
{
data.Add(direction, alignment);
}
}
return data;
}
public static TheoryData<FlexDirection, JustifyContent> GetJustifyContentValues()
{
var data = new TheoryData<FlexDirection, JustifyContent>();
foreach (var direction in Enum.GetValues<FlexDirection>())
{
foreach (var justify in Enum.GetValues<JustifyContent>())
{
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);
}
}
}
Loading…
Cancel
Save