diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs
new file mode 100644
index 0000000..e69de29
diff --git a/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml b/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml
new file mode 100644
index 0000000..a6174bc
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml.cs b/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml.cs
new file mode 100644
index 0000000..f405240
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia.Controls;
+using Ursa.Demo.ViewModels;
+
+namespace Ursa.Demo.Pages;
+
+public partial class ElasticWrapPanelDemo : UserControl
+{
+ public ElasticWrapPanelDemo()
+ {
+ InitializeComponent();
+ DataContext = new ElasticWrapPanelDemoViewModel();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs
new file mode 100644
index 0000000..4964f36
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs
@@ -0,0 +1,54 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.ViewModels;
+
+public partial class ElasticWrapPanelDemoViewModel : ObservableObject
+{
+ [ObservableProperty] private Orientation _selectedOrientation = Orientation.Horizontal;
+ [ObservableProperty] private ScrollBarVisibility _horizontalVisibility = ScrollBarVisibility.Auto;
+ [ObservableProperty] private ScrollBarVisibility _verticalVisibility = ScrollBarVisibility.Auto;
+
+ [ObservableProperty] private bool _isFillHorizontal;
+ [ObservableProperty] private bool _isFillVertical;
+ [ObservableProperty] private double _itemWidth = 40d;
+ [ObservableProperty] private double _itemHeight = 40d;
+
+ [ObservableProperty] private bool _autoWidth = true;
+ [ObservableProperty] private bool _autoHeight = true;
+ [ObservableProperty] private double _itemSelfWidth = double.NaN;
+ [ObservableProperty] private double _itemSelfHeight = double.NaN;
+
+ [ObservableProperty] private HorizontalAlignment _cmbHAlign = HorizontalAlignment.Left;
+ [ObservableProperty] private VerticalAlignment _cmbVAlign = VerticalAlignment.Stretch;
+
+ private double _oldItemSelfWidth;
+ private double _oldItemSelfHeight;
+
+ partial void OnAutoWidthChanged(bool value)
+ {
+ if (value)
+ {
+ _oldItemSelfWidth = ItemSelfWidth;
+ ItemSelfWidth = double.NaN;
+ }
+ else
+ {
+ ItemSelfWidth = _oldItemSelfWidth;
+ }
+ }
+
+ partial void OnAutoHeightChanged(bool value)
+ {
+ if (value)
+ {
+ _oldItemSelfHeight = ItemSelfHeight;
+ ItemSelfHeight = double.NaN;
+ }
+ else
+ {
+ ItemSelfHeight = _oldItemSelfHeight;
+ }
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 89a8532..1baf568 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -36,6 +36,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyDisableContainer => new DisableContainerDemoViewModel(),
MenuKeys.MenuKeyDrawer => new DrawerDemoViewModel(),
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
+ MenuKeys.MenuKeyElasticWrapPanel => new ElasticWrapPanelDemoViewModel(),
MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(),
MenuKeys.MenuKeyForm => new FormDemoViewModel(),
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index a6c1b79..e196a85 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -23,6 +23,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
new() { MenuHeader = "Drawer", Key = MenuKeys.MenuKeyDrawer },
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
+ new() { MenuHeader = "ElasticWrapPanel", Key = MenuKeys.MenuKeyElasticWrapPanel },
new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector },
new() { MenuHeader = "Form", Key = MenuKeys.MenuKeyForm },
new() { MenuHeader = "Icon Button", Key = MenuKeys.MenuKeyIconButton },
@@ -69,6 +70,7 @@ public static class MenuKeys
public const string MenuKeyDisableContainer = "DisableContainer";
public const string MenuKeyDrawer = "Drawer";
public const string MenuKeyDualBadge = "DualBadge";
+ public const string MenuKeyElasticWrapPanel = "ElasticWrapPanel";
public const string MenuKeyEnumSelector = "EnumSelector";
public const string MenuKeyForm = "Form";
public const string MenuKeyImageViewer = "ImageViewer";
diff --git a/src/Ursa.Themes.Semi/Controls/ElasticWrapPanel.axaml b/src/Ursa.Themes.Semi/Controls/ElasticWrapPanel.axaml
new file mode 100644
index 0000000..fecc72d
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/ElasticWrapPanel.axaml
@@ -0,0 +1,3 @@
+
+
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index 23ba36e..4d7ae82 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -13,6 +13,7 @@
+
diff --git a/src/Ursa/Controls/ElasticWrapPanel.cs b/src/Ursa/Controls/ElasticWrapPanel.cs
new file mode 100644
index 0000000..cf799df
--- /dev/null
+++ b/src/Ursa/Controls/ElasticWrapPanel.cs
@@ -0,0 +1,481 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Utilities;
+using static System.Math;
+
+namespace Ursa.Controls;
+
+public class ElasticWrapPanel : WrapPanel
+{
+ static ElasticWrapPanel()
+ {
+ IsFillHorizontalProperty.Changed.AddClassHandler(OnIsFillPropertyChanged);
+ IsFillVerticalProperty.Changed.AddClassHandler(OnIsFillPropertyChanged);
+
+ AffectsMeasure(IsFillHorizontalProperty, IsFillVerticalProperty);
+ }
+
+ #region AttachedProperty
+
+ public static void SetFixToRB(Control element, bool value)
+ {
+ _ = element ?? throw new ArgumentNullException(nameof(element));
+ element.SetValue(FixToRBProperty, value);
+ }
+
+ public static bool GetIsFixToRB(Control element)
+ {
+ _ = element ?? throw new ArgumentNullException(nameof(element));
+ return element.GetValue(FixToRBProperty);
+ }
+
+ ///
+ /// Fixed to [Right (Horizontal Mode) | Bottom (Vertical Mode)]
+ /// which will cause line breaks
+ ///
+ public static readonly AttachedProperty FixToRBProperty =
+ AvaloniaProperty.RegisterAttached("FixToRB");
+
+ #endregion
+
+ #region StyledProperty
+
+ public bool IsFillHorizontal
+ {
+ get => GetValue(IsFillHorizontalProperty);
+ set => SetValue(IsFillHorizontalProperty, value);
+ }
+
+ public static readonly StyledProperty IsFillHorizontalProperty =
+ AvaloniaProperty.Register(nameof(IsFillHorizontal));
+
+ public bool IsFillVertical
+ {
+ get => GetValue(IsFillVerticalProperty);
+ set => SetValue(IsFillVerticalProperty, value);
+ }
+
+ public static readonly StyledProperty IsFillVerticalProperty =
+ AvaloniaProperty.Register(nameof(IsFillVertical));
+
+ private static void OnIsFillPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
+ {
+ (d as ElasticWrapPanel)?.InvalidateMeasure();
+ }
+
+ #endregion
+
+ protected override Size MeasureOverride(Size constraint)
+ {
+ double itemWidth = ItemWidth;
+ double itemHeight = ItemHeight;
+ var orientation = Orientation;
+ var children = Children;
+
+ // Determine the space required for items in the same row/column based on horizontal/vertical arrangement
+ var curLineSize = new UVSize(orientation);
+
+ // Calculate the total space requirement for this ElasticWrapPanel
+ var panelSize = new UVSize(orientation);
+
+ // Measure UVSize with the given space constraint, used for measuring elements when ItemWidth and ItemHeight are not set
+ var uvConstraint = new UVSize(orientation, constraint.Width, constraint.Height);
+ bool itemWidthSet = !double.IsNaN(itemWidth);
+ bool itemHeightSet = !double.IsNaN(itemHeight);
+
+ var childConstraint = new Size(
+ itemWidthSet ? itemWidth : constraint.Width,
+ itemHeightSet ? itemHeight : constraint.Height);
+
+ // Measurement space for elements with FixToRB=True
+ Size childFixConstraint = new Size(constraint.Width, constraint.Height);
+ switch (orientation)
+ {
+ case Orientation.Horizontal when itemHeightSet:
+ childFixConstraint = new Size(constraint.Width, itemHeight);
+ break;
+ case Orientation.Vertical when itemWidthSet:
+ childFixConstraint = new Size(itemWidth, constraint.Height);
+ break;
+ }
+
+ // This is the size for non-space measurement
+ UVSize itemSetSize = new UVSize(orientation,
+ itemWidthSet ? itemWidth : 0,
+ itemHeightSet ? itemHeight : 0);
+
+ foreach (var child in children)
+ {
+ UVSize sz;
+ if (GetIsFixToRB(child))
+ {
+ // Measure the element when it needs to be fixed to the right/bottom
+ child.Measure(childFixConstraint);
+ sz = new UVSize(orientation, child.DesiredSize.Width, child.DesiredSize.Height);
+
+ // Ensure the width/height is within the constraint limits
+ if (sz.U > 0 && itemSetSize.U > 0)
+ {
+ if (sz.U < itemSetSize.U)
+ {
+ sz.U = itemSetSize.U;
+ }
+ else
+ {
+ sz.U = Min(sz.U, uvConstraint.U);
+ }
+ }
+
+ if (sz.V > 0 && itemSetSize.V > 0 && sz.V < itemSetSize.V)
+ {
+ sz.V = itemSetSize.V;
+ }
+
+ if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U))
+ {
+ panelSize.U = Max(curLineSize.U, panelSize.U);
+ panelSize.V += curLineSize.V;
+ curLineSize = sz;
+ }
+ else
+ {
+ curLineSize.U += sz.U;
+ curLineSize.V = Max(sz.V, curLineSize.V);
+ panelSize.U = Max(curLineSize.U, panelSize.U);
+ panelSize.V += curLineSize.V;
+ }
+
+ curLineSize = new UVSize(orientation);
+ }
+ else
+ {
+ // Flow passes its own constraint to children
+ child.Measure(childConstraint);
+
+ // This is the size of the child in UV space
+ sz = new UVSize(orientation,
+ itemWidthSet ? itemWidth : child.DesiredSize.Width,
+ itemHeightSet ? itemHeight : child.DesiredSize.Height);
+
+ if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) // Need to switch to another line
+ {
+ panelSize.U = Max(curLineSize.U, panelSize.U);
+ panelSize.V += curLineSize.V;
+ curLineSize = sz;
+
+ if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) // The element is wider than the constraint - give it a separate line
+ {
+ panelSize.U = Max(sz.U, panelSize.U);
+ panelSize.V += sz.V;
+ curLineSize = new UVSize(orientation);
+ }
+ }
+ else // Continue to accumulate a line
+ {
+ curLineSize.U += sz.U;
+ curLineSize.V = Max(sz.V, curLineSize.V);
+ }
+ }
+ }
+
+ // The last line size, if any should be added
+ panelSize.U = 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 finalSize)
+ {
+ bool itemWidthSet = !double.IsNaN(ItemWidth);
+ bool itemHeightSet = !double.IsNaN(ItemHeight);
+
+ // This is the size for non-space measurement
+ UVSize itemSetSize = new UVSize(Orientation,
+ itemWidthSet ? ItemWidth : 0,
+ itemHeightSet ? ItemHeight : 0);
+
+ // Measure UVSize with the given space constraint, used for measuring elements when ItemWidth and ItemHeight are not set
+ UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
+
+ // Collection of elements in the same direction (row/column)
+ List lineUVCollection = new List();
+
+ #region Get the collection of elements in the same direction
+
+ // Current collection of elements in a row/column
+ UVCollection curLineUIs = new UVCollection(Orientation, itemSetSize);
+
+ // Iterate over the child elements
+ var children = Children;
+ foreach (var child in children)
+ {
+ UVSize sz;
+ if (GetIsFixToRB(child))
+ {
+ // Measure the element when it needs to be fixed to the right/bottom
+ sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
+ double lengthCount = 1;
+ if (sz.U > 0 && itemSetSize.U > 0)
+ {
+ if (sz.U < itemSetSize.U)
+ {
+ sz.U = itemSetSize.U;
+ }
+ else
+ {
+ lengthCount = Ceiling(sz.U / itemSetSize.U);
+ sz.U = Min(sz.U, uvFinalSize.U);
+ }
+ }
+
+ if (sz.V > 0 && itemSetSize.V > 0 && sz.V < itemSetSize.V)
+ {
+ sz.V = itemSetSize.V;
+ }
+
+ if (MathUtilities.GreaterThan(curLineUIs.TotalU + sz.U, uvFinalSize.U))
+ {
+ if (curLineUIs.Count > 0)
+ {
+ lineUVCollection.Add(curLineUIs);
+ }
+
+ curLineUIs = new UVCollection(Orientation, itemSetSize);
+ curLineUIs.Add(child, sz, Convert.ToInt32(lengthCount));
+ }
+ else
+ {
+ curLineUIs.Add(child, sz, Convert.ToInt32(lengthCount));
+ }
+
+ lineUVCollection.Add(curLineUIs);
+ curLineUIs = new UVCollection(Orientation, itemSetSize);
+ }
+ else
+ {
+ sz = new UVSize(Orientation,
+ itemWidthSet ? ItemWidth : child.DesiredSize.Width,
+ itemHeightSet ? ItemHeight : child.DesiredSize.Height);
+
+ if (MathUtilities.GreaterThan(curLineUIs.TotalU + sz.U, uvFinalSize.U)) // Need to switch to another line
+ {
+ if (curLineUIs.Count > 0)
+ {
+ lineUVCollection.Add(curLineUIs);
+ }
+
+ curLineUIs = new UVCollection(Orientation, itemSetSize);
+ curLineUIs.Add(child, sz);
+ if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U))
+ {
+ lineUVCollection.Add(curLineUIs);
+ curLineUIs = new UVCollection(Orientation, itemSetSize);
+ }
+ }
+ else
+ {
+ curLineUIs.Add(child, sz);
+ }
+ }
+ }
+
+ if (curLineUIs.Count > 0 && !lineUVCollection.Contains(curLineUIs))
+ {
+ lineUVCollection.Add(curLineUIs);
+ }
+
+ #endregion
+
+ bool isFillU = false;
+ bool isFillV = false;
+ switch (Orientation)
+ {
+ case Orientation.Horizontal:
+ isFillU = IsFillHorizontal;
+ isFillV = IsFillVertical;
+ break;
+
+ case Orientation.Vertical:
+ isFillU = IsFillVertical;
+ isFillV = IsFillHorizontal;
+ break;
+ }
+
+ if (lineUVCollection.Count > 0)
+ {
+ double accumulatedV = 0;
+ double adaptULength = 0;
+ bool isAdaptV = false;
+ double adaptVLength = 0;
+ if (isFillU)
+ {
+ if (itemSetSize.U > 0)
+ {
+ int maxElementCount = lineUVCollection
+ .Max(uiSet => uiSet.UICollection
+ .Sum(p => p.Value.ULengthCount));
+ adaptULength = (uvFinalSize.U - maxElementCount * itemSetSize.U) / maxElementCount;
+ adaptULength = Max(adaptULength, 0);
+ }
+ }
+
+ if (isFillV)
+ {
+ if (itemSetSize.V > 0)
+ {
+ isAdaptV = true;
+ adaptVLength = uvFinalSize.V / lineUVCollection.Count;
+ }
+ }
+
+ bool isHorizontal = Orientation == Orientation.Horizontal;
+ foreach (var uvCollection in lineUVCollection)
+ {
+ double u = 0;
+ var lineUIEles = uvCollection.UICollection.Keys.ToList();
+ double linevV = isAdaptV ? adaptVLength : uvCollection.LineV;
+ foreach (var child in lineUIEles)
+ {
+ UVLengthSize childSize = uvCollection.UICollection[child];
+
+ double layoutSlotU = childSize.UVSize.U + childSize.ULengthCount * adaptULength;
+ double layoutSlotV = isAdaptV ? linevV : childSize.UVSize.V;
+ if (ElasticWrapPanel.GetIsFixToRB(child) == false)
+ {
+ child.Arrange(new Rect(
+ isHorizontal ? u : accumulatedV,
+ isHorizontal ? accumulatedV : u,
+ isHorizontal ? layoutSlotU : layoutSlotV,
+ isHorizontal ? layoutSlotV : layoutSlotU));
+ }
+ else
+ {
+ if (itemSetSize.U > 0)
+ {
+ layoutSlotU = childSize.ULengthCount * itemSetSize.U +
+ childSize.ULengthCount * adaptULength;
+ double leaveULength = uvFinalSize.U - u;
+ layoutSlotU = Min(leaveULength, layoutSlotU);
+ }
+
+ child.Arrange(new Rect(
+ isHorizontal ? Max(0, uvFinalSize.U - layoutSlotU) : accumulatedV,
+ isHorizontal ? accumulatedV : Max(0, uvFinalSize.U - layoutSlotU),
+ isHorizontal ? layoutSlotU : layoutSlotV,
+ isHorizontal ? layoutSlotV : layoutSlotU));
+ }
+
+ u += layoutSlotU;
+ }
+
+ accumulatedV += linevV;
+ lineUIEles.Clear();
+ }
+ }
+
+ lineUVCollection.ForEach(col => col.Dispose());
+ lineUVCollection.Clear();
+ return finalSize;
+ }
+
+ #region Protected Methods
+
+ private struct UVSize
+ {
+ internal UVSize(Orientation orientation, double width, double height)
+ {
+ U = V = 0d;
+ _orientation = orientation;
+ Width = width;
+ Height = height;
+ }
+
+ internal UVSize(Orientation orientation)
+ {
+ U = V = 0d;
+ _orientation = orientation;
+ }
+
+ internal double U;
+ internal double V;
+ private Orientation _orientation;
+
+ internal double Width
+ {
+ get { return _orientation == Orientation.Horizontal ? U : V; }
+ set
+ {
+ if (_orientation == Orientation.Horizontal) U = value;
+ else V = value;
+ }
+ }
+
+ internal double Height
+ {
+ get { return _orientation == Orientation.Horizontal ? V : U; }
+ set
+ {
+ if (_orientation == Orientation.Horizontal) V = value;
+ else U = value;
+ }
+ }
+ }
+
+ private class UVLengthSize
+ {
+ public UVSize UVSize { get; set; }
+
+ public int ULengthCount { get; set; }
+
+ public UVLengthSize(UVSize uvSize, int uLengthCount)
+ {
+ this.UVSize = uvSize;
+ this.ULengthCount = uLengthCount;
+ }
+ }
+
+ ///
+ /// Elements used to store the same row/column
+ ///
+ private class UVCollection : IDisposable
+ {
+ public Dictionary UICollection { get; }
+
+ private UVSize LineDesireUVSize;
+
+ private UVSize ItemSetSize;
+
+ public UVCollection(Orientation orientation, UVSize itemSetSize)
+ {
+ UICollection = new Dictionary();
+ LineDesireUVSize = new UVSize(orientation);
+ ItemSetSize = itemSetSize;
+ }
+
+ public double TotalU => LineDesireUVSize.U;
+
+ public double LineV => LineDesireUVSize.V;
+
+ public void Add(Control element, UVSize childSize, int itemULength = 1)
+ {
+ if (UICollection.ContainsKey(element))
+ throw new InvalidOperationException("The element already exists and cannot be added repeatedly.");
+
+ UICollection[element] = new UVLengthSize(childSize, itemULength);
+ LineDesireUVSize.U += childSize.U;
+ LineDesireUVSize.V = Max(LineDesireUVSize.V, childSize.V);
+ }
+
+ public int Count => UICollection.Count;
+
+ public void Dispose()
+ {
+ UICollection.Clear();
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file