committed by
GitHub
18 changed files with 2139 additions and 1 deletions
@ -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(); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue