using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using HandyControl.Data; using HandyControl.Expression.Drawing; using HandyControl.Tools; using HandyControl.Tools.Extension; namespace HandyControl.Controls; public class FlexPanel : Panel { private UVSize _uvConstraint; private int _lineCount; private readonly List _orderList = new(); #region Item public static readonly DependencyProperty OrderProperty = DependencyProperty.RegisterAttached( "Order", typeof(int), typeof(FlexPanel), new FrameworkPropertyMetadata(ValueBoxes.Int0Box, OnItemPropertyChanged)); public static void SetOrder(DependencyObject element, int value) => element.SetValue(OrderProperty, value); public static int GetOrder(DependencyObject element) => (int) element.GetValue(OrderProperty); public static readonly DependencyProperty FlexGrowProperty = DependencyProperty.RegisterAttached( "FlexGrow", typeof(double), typeof(FlexPanel), new FrameworkPropertyMetadata(ValueBoxes.Double0Box, OnItemPropertyChanged), ValidateHelper.IsInRangeOfPosDoubleIncludeZero); public static void SetFlexGrow(DependencyObject element, double value) => element.SetValue(FlexGrowProperty, value); public static double GetFlexGrow(DependencyObject element) => (double) element.GetValue(FlexGrowProperty); public static readonly DependencyProperty FlexShrinkProperty = DependencyProperty.RegisterAttached( "FlexShrink", typeof(double), typeof(FlexPanel), new FrameworkPropertyMetadata(ValueBoxes.Double1Box, OnItemPropertyChanged), ValidateHelper.IsInRangeOfPosDoubleIncludeZero); public static void SetFlexShrink(DependencyObject element, double value) => element.SetValue(FlexShrinkProperty, value); public static double GetFlexShrink(DependencyObject element) => (double) element.GetValue(FlexShrinkProperty); public static readonly DependencyProperty FlexBasisProperty = DependencyProperty.RegisterAttached( "FlexBasis", typeof(double), typeof(FlexPanel), new FrameworkPropertyMetadata(double.NaN, OnItemPropertyChanged)); public static void SetFlexBasis(DependencyObject element, double value) => element.SetValue(FlexBasisProperty, value); public static double GetFlexBasis(DependencyObject element) => (double) element.GetValue(FlexBasisProperty); public static readonly DependencyProperty AlignSelfProperty = DependencyProperty.RegisterAttached( "AlignSelf", typeof(FlexItemAlignment), typeof(FlexPanel), new FrameworkPropertyMetadata(default(FlexItemAlignment), OnItemPropertyChanged)); public static void SetAlignSelf(DependencyObject element, FlexItemAlignment value) => element.SetValue(AlignSelfProperty, value); #endregion #region Panel public static FlexItemAlignment GetAlignSelf(DependencyObject element) => (FlexItemAlignment) element.GetValue(AlignSelfProperty); public static readonly DependencyProperty FlexDirectionProperty = DependencyProperty.Register( nameof(FlexDirection), typeof(FlexDirection), typeof(FlexPanel), new FrameworkPropertyMetadata(default(FlexDirection), FrameworkPropertyMetadataOptions.AffectsMeasure)); public FlexDirection FlexDirection { get => (FlexDirection) GetValue(FlexDirectionProperty); set => SetValue(FlexDirectionProperty, value); } public static readonly DependencyProperty FlexWrapProperty = DependencyProperty.Register( nameof(FlexWrap), typeof(FlexWrap), typeof(FlexPanel), new FrameworkPropertyMetadata(default(FlexWrap), FrameworkPropertyMetadataOptions.AffectsMeasure)); public FlexWrap FlexWrap { get => (FlexWrap) GetValue(FlexWrapProperty); set => SetValue(FlexWrapProperty, value); } public static readonly DependencyProperty JustifyContentProperty = DependencyProperty.Register( nameof(JustifyContent), typeof(FlexContentJustify), typeof(FlexPanel), new FrameworkPropertyMetadata(default(FlexContentJustify), FrameworkPropertyMetadataOptions.AffectsMeasure)); public FlexContentJustify JustifyContent { get => (FlexContentJustify) GetValue(JustifyContentProperty); set => SetValue(JustifyContentProperty, value); } public static readonly DependencyProperty AlignItemsProperty = DependencyProperty.Register( nameof(AlignItems), typeof(FlexItemsAlignment), typeof(FlexPanel), new FrameworkPropertyMetadata(default(FlexItemsAlignment), FrameworkPropertyMetadataOptions.AffectsMeasure)); public FlexItemsAlignment AlignItems { get => (FlexItemsAlignment) GetValue(AlignItemsProperty); set => SetValue(AlignItemsProperty, value); } public static readonly DependencyProperty AlignContentProperty = DependencyProperty.Register( nameof(AlignContent), typeof(FlexContentAlignment), typeof(FlexPanel), new FrameworkPropertyMetadata(default(FlexContentAlignment), FrameworkPropertyMetadataOptions.AffectsMeasure)); public FlexContentAlignment AlignContent { get => (FlexContentAlignment) GetValue(AlignContentProperty); set => SetValue(AlignContentProperty, value); } #endregion private static void OnItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is UIElement element) { if (VisualTreeHelper.GetParent(element) is FlexPanel p) { p.InvalidateMeasure(); } } } protected override Size MeasureOverride(Size constraint) { var flexDirection = FlexDirection; var flexWrap = FlexWrap; var curLineSize = new UVSize(flexDirection); var panelSize = new UVSize(flexDirection); _uvConstraint = new UVSize(flexDirection, constraint); var childConstraint = new Size(constraint.Width, constraint.Height); _lineCount = 1; var children = InternalChildren; _orderList.Clear(); for (var i = 0; i < children.Count; i++) { var child = children[i]; if (child == null) continue; _orderList.Add(new FlexItemInfo(i, GetOrder(child))); } _orderList.Sort(); for (var i = 0; i < children.Count; i++) { var child = children[_orderList[i].Index]; if (child == null) continue; var flexBasis = GetFlexBasis(child); if (!flexBasis.IsNaN()) { child.SetCurrentValue(WidthProperty, flexBasis); } child.Measure(childConstraint); var sz = new UVSize(flexDirection, child.DesiredSize); if (flexWrap == FlexWrap.NoWrap) //continue to accumulate a line { curLineSize.U += sz.U; curLineSize.V = Math.Max(sz.V, curLineSize.V); } else { if (MathHelper.GreaterThan(curLineSize.U + sz.U, _uvConstraint.U)) //need to switch to another line { panelSize.U = Math.Max(curLineSize.U, panelSize.U); panelSize.V += curLineSize.V; curLineSize = sz; _lineCount++; if (MathHelper.GreaterThan(sz.U, _uvConstraint.U)) //the element is wider then the constrint - give it a separate line { panelSize.U = Math.Max(sz.U, panelSize.U); panelSize.V += sz.V; curLineSize = new UVSize(flexDirection); _lineCount++; } } else //continue to accumulate a line { curLineSize.U += sz.U; curLineSize.V = Math.Max(sz.V, curLineSize.V); } } } //the last line size, if any should be added panelSize.U = Math.Max(curLineSize.U, panelSize.U); panelSize.V += curLineSize.V; //go from UV space to W/H space return new Size(panelSize.Width, panelSize.Height); } protected override Size ArrangeOverride(Size arrangeSize) { var flexDirection = FlexDirection; var flexWrap = FlexWrap; var alignContent = AlignContent; var uvFinalSize = new UVSize(flexDirection, arrangeSize); if (MathHelper.IsZero(uvFinalSize.U) || MathHelper.IsZero(uvFinalSize.V)) return arrangeSize; // init status var children = InternalChildren; var lineIndex = 0; var curLineSizeArr = new UVSize[_lineCount]; curLineSizeArr[0] = new UVSize(flexDirection); var lastInLineArr = new int[_lineCount]; for (var i = 0; i < _lineCount; i++) { lastInLineArr[i] = int.MaxValue; } // calculate line max U space for (var i = 0; i < children.Count; i++) { var child = children[_orderList[i].Index]; if (child == null) continue; var sz = new UVSize(flexDirection, child.DesiredSize); if (flexWrap == FlexWrap.NoWrap) { curLineSizeArr[lineIndex].U += sz.U; curLineSizeArr[lineIndex].V = Math.Max(sz.V, curLineSizeArr[lineIndex].V); } else { if (MathHelper.GreaterThan(curLineSizeArr[lineIndex].U + sz.U, uvFinalSize.U)) //need to switch to another line { lastInLineArr[lineIndex] = i; lineIndex++; curLineSizeArr[lineIndex] = sz; if (MathHelper.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line { //switch to next line which only contain one element lastInLineArr[lineIndex] = i; lineIndex++; curLineSizeArr[lineIndex] = new UVSize(flexDirection); } } else //continue to accumulate a line { curLineSizeArr[lineIndex].U += sz.U; curLineSizeArr[lineIndex].V = Math.Max(sz.V, curLineSizeArr[lineIndex].V); } } } // init status var scaleU = Math.Min(_uvConstraint.U / uvFinalSize.U, 1); var firstInLine = 0; var wrapReverseAdd = 0; var wrapReverseFlag = flexWrap == FlexWrap.WrapReverse ? -1 : 1; var accumulatedFlag = flexWrap == FlexWrap.WrapReverse ? 1 : 0; var itemsU = .0; var accumulatedV = .0; var freeV = uvFinalSize.V; foreach (var flexSize in curLineSizeArr) { freeV -= flexSize.V; } var freeItemV = freeV; // calculate status var lineFreeVArr = new double[_lineCount]; switch (alignContent) { case FlexContentAlignment.Stretch: if (_lineCount > 1) { freeItemV = freeV / _lineCount; for (var i = 0; i < _lineCount; i++) { lineFreeVArr[i] = freeItemV; } accumulatedV = flexWrap == FlexWrap.WrapReverse ? uvFinalSize.V - curLineSizeArr[0].V - lineFreeVArr[0] : 0; } break; case FlexContentAlignment.FlexStart: wrapReverseAdd = flexWrap == FlexWrap.WrapReverse ? 0 : 1; if (_lineCount > 1) { accumulatedV = flexWrap == FlexWrap.WrapReverse ? uvFinalSize.V - curLineSizeArr[0].V : 0; } else { wrapReverseAdd = 0; } break; case FlexContentAlignment.FlexEnd: wrapReverseAdd = flexWrap == FlexWrap.WrapReverse ? 1 : 0; if (_lineCount > 1) { accumulatedV = flexWrap == FlexWrap.WrapReverse ? uvFinalSize.V - curLineSizeArr[0].V - freeV : freeV; } else { wrapReverseAdd = 0; } break; case FlexContentAlignment.Center: if (_lineCount > 1) { accumulatedV = flexWrap == FlexWrap.WrapReverse ? uvFinalSize.V - curLineSizeArr[0].V - freeV * 0.5 : freeV * 0.5; } break; case FlexContentAlignment.SpaceBetween: if (_lineCount > 1) { freeItemV = freeV / (_lineCount - 1); for (var i = 0; i < _lineCount - 1; i++) { lineFreeVArr[i] = freeItemV; } accumulatedV = flexWrap == FlexWrap.WrapReverse ? uvFinalSize.V - curLineSizeArr[0].V : 0; } break; case FlexContentAlignment.SpaceAround: if (_lineCount > 1) { freeItemV = freeV / _lineCount * 0.5; for (var i = 0; i < _lineCount - 1; i++) { lineFreeVArr[i] = freeItemV * 2; } accumulatedV = flexWrap == FlexWrap.WrapReverse ? uvFinalSize.V - curLineSizeArr[0].V - freeItemV : freeItemV; } break; } // clear status lineIndex = 0; // arrange line for (var i = 0; i < children.Count; i++) { var child = children[_orderList[i].Index]; if (child == null) continue; var sz = new UVSize(flexDirection, child.DesiredSize); if (flexWrap != FlexWrap.NoWrap) { if (i >= lastInLineArr[lineIndex]) //need to switch to another line { ArrangeLine(new FlexLineInfo { ItemsU = itemsU, OffsetV = accumulatedV + freeItemV * wrapReverseAdd, LineV = curLineSizeArr[lineIndex].V, LineFreeV = freeItemV, LineU = uvFinalSize.U, ItemStartIndex = firstInLine, ItemEndIndex = i, ScaleU = scaleU }); accumulatedV += (lineFreeVArr[lineIndex] + curLineSizeArr[lineIndex + accumulatedFlag].V) * wrapReverseFlag; lineIndex++; itemsU = 0; if (i >= lastInLineArr[lineIndex]) //the element is wider then the constraint - give it a separate line { //switch to next line which only contain one element ArrangeLine(new FlexLineInfo { ItemsU = itemsU, OffsetV = accumulatedV + freeItemV * wrapReverseAdd, LineV = curLineSizeArr[lineIndex].V, LineFreeV = freeItemV, LineU = uvFinalSize.U, ItemStartIndex = i, ItemEndIndex = ++i, ScaleU = scaleU }); accumulatedV += (lineFreeVArr[lineIndex] + curLineSizeArr[lineIndex + accumulatedFlag].V) * wrapReverseFlag; lineIndex++; itemsU = 0; } firstInLine = i; } } itemsU += sz.U; } // arrange the last line, if any if (firstInLine < children.Count) { ArrangeLine(new FlexLineInfo { ItemsU = itemsU, OffsetV = accumulatedV + freeItemV * wrapReverseAdd, LineV = curLineSizeArr[lineIndex].V, LineFreeV = freeItemV, LineU = uvFinalSize.U, ItemStartIndex = firstInLine, ItemEndIndex = children.Count, ScaleU = scaleU }); } return arrangeSize; } private void ArrangeLine(FlexLineInfo lineInfo) { var flexDirection = FlexDirection; var flexWrap = FlexWrap; var justifyContent = JustifyContent; var alignItems = AlignItems; var isHorizontal = flexDirection == FlexDirection.Row || flexDirection == FlexDirection.RowReverse; var isReverse = flexDirection == FlexDirection.RowReverse || flexDirection == FlexDirection.ColumnReverse; var itemCount = lineInfo.ItemEndIndex - lineInfo.ItemStartIndex; var children = InternalChildren; var lineFreeU = lineInfo.LineU - lineInfo.ItemsU; var constraintFreeU = _uvConstraint.U - lineInfo.ItemsU; // calculate initial u var u = .0; if (isReverse) { u = justifyContent switch { FlexContentJustify.FlexStart => lineInfo.LineU, FlexContentJustify.SpaceBetween => lineInfo.LineU, FlexContentJustify.SpaceAround => lineInfo.LineU, FlexContentJustify.FlexEnd => lineInfo.ItemsU, FlexContentJustify.Center => (lineInfo.LineU + lineInfo.ItemsU) * 0.5, _ => u }; } else { u = justifyContent switch { FlexContentJustify.FlexEnd => lineFreeU, FlexContentJustify.Center => lineFreeU * 0.5, _ => u }; } u *= lineInfo.ScaleU; // apply FlexGrow var flexGrowUArr = new double[itemCount]; if (constraintFreeU > 0) { var ignoreFlexGrow = true; var flexGrowSum = .0; for (var i = 0; i < itemCount; i++) { var flexGrow = GetFlexGrow(children[_orderList[i].Index]); ignoreFlexGrow &= MathHelper.IsVerySmall(flexGrow); flexGrowUArr[i] = flexGrow; flexGrowSum += flexGrow; } if (!ignoreFlexGrow) { var flexGrowItem = .0; if (flexGrowSum > 0) { flexGrowItem = constraintFreeU / flexGrowSum; lineInfo.ScaleU = 1; lineFreeU = 0; //line free U was used up } for (var i = 0; i < itemCount; i++) { flexGrowUArr[i] *= flexGrowItem; } } else { flexGrowUArr = new double[itemCount]; } } // apply FlexShrink var flexShrinkUArr = new double[itemCount]; if (constraintFreeU < 0) { var ignoreFlexShrink = true; var flexShrinkSum = .0; for (var i = 0; i < itemCount; i++) { var flexShrink = GetFlexShrink(children[_orderList[i].Index]); ignoreFlexShrink &= MathHelper.IsVerySmall(flexShrink - 1); flexShrinkUArr[i] = flexShrink; flexShrinkSum += flexShrink; } if (!ignoreFlexShrink) { var flexShrinkItem = .0; if (flexShrinkSum > 0) { flexShrinkItem = constraintFreeU / flexShrinkSum; lineInfo.ScaleU = 1; lineFreeU = 0; //line free U was used up } for (var i = 0; i < itemCount; i++) { flexShrinkUArr[i] *= flexShrinkItem; } } else { flexShrinkUArr = new double[itemCount]; } } // calculate offset u var offsetUArr = new double[itemCount]; if (lineFreeU > 0) { if (justifyContent == FlexContentJustify.SpaceBetween) { var freeItemU = lineFreeU / (itemCount - 1); for (var i = 1; i < itemCount; i++) { offsetUArr[i] = freeItemU; } } else if (justifyContent == FlexContentJustify.SpaceAround) { var freeItemU = lineFreeU / itemCount * 0.5; offsetUArr[0] = freeItemU; for (var i = 1; i < itemCount; i++) { offsetUArr[i] = freeItemU * 2; } } } // arrange item for (int i = lineInfo.ItemStartIndex, j = 0; i < lineInfo.ItemEndIndex; i++, j++) { var child = children[_orderList[i].Index]; if (child == null) continue; var childSize = new UVSize(flexDirection, isHorizontal ? new Size(child.DesiredSize.Width * lineInfo.ScaleU, child.DesiredSize.Height) : new Size(child.DesiredSize.Width, child.DesiredSize.Height * lineInfo.ScaleU)); childSize.U += flexGrowUArr[j] + flexShrinkUArr[j]; if (isReverse) { u -= childSize.U; u -= offsetUArr[j]; } else { u += offsetUArr[j]; } var v = lineInfo.OffsetV; var alignSelf = GetAlignSelf(child); var alignment = alignSelf == FlexItemAlignment.Auto ? alignItems : (FlexItemsAlignment) alignSelf; switch (alignment) { case FlexItemsAlignment.Stretch: if (_lineCount == 1 && flexWrap == FlexWrap.NoWrap) { childSize.V = lineInfo.LineV + lineInfo.LineFreeV; } else { childSize.V = lineInfo.LineV; } break; case FlexItemsAlignment.FlexEnd: v += lineInfo.LineV - childSize.V; break; case FlexItemsAlignment.Center: v += (lineInfo.LineV - childSize.V) * 0.5; break; } child.Arrange(isHorizontal ? new Rect(u, v, childSize.U, childSize.V) : new Rect(v, u, childSize.V, childSize.U)); if (!isReverse) { u += childSize.U; } } } private readonly struct FlexItemInfo : IComparable { public FlexItemInfo(int index, int order) { Index = index; Order = order; } private int Order { get; } public int Index { get; } public int CompareTo(FlexItemInfo other) { var orderCompare = Order.CompareTo(other.Order); if (orderCompare != 0) return orderCompare; return Index.CompareTo(other.Index); } } private struct FlexLineInfo { public double ItemsU { get; set; } public double OffsetV { get; set; } public double LineU { get; set; } public double LineV { get; set; } public double LineFreeV { get; set; } public int ItemStartIndex { get; set; } public int ItemEndIndex { get; set; } public double ScaleU { get; set; } } private struct UVSize { public UVSize(FlexDirection direction, Size size) { U = V = 0d; FlexDirection = direction; Width = size.Width; Height = size.Height; } public UVSize(FlexDirection direction) { U = V = 0d; FlexDirection = direction; } public double U { get; set; } public double V { get; set; } private FlexDirection FlexDirection { get; } public double Width { get => FlexDirection == FlexDirection.Row || FlexDirection == FlexDirection.RowReverse ? U : V; private set { if (FlexDirection == FlexDirection.Row || FlexDirection == FlexDirection.RowReverse) { U = value; } else { V = value; } } } public double Height { get => FlexDirection == FlexDirection.Row || FlexDirection == FlexDirection.RowReverse ? V : U; private set { if (FlexDirection == FlexDirection.Row || FlexDirection == FlexDirection.RowReverse) { V = value; } else { U = value; } } } } }