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