diff --git a/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml b/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml
index 10040d3..d439086 100644
--- a/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml
+++ b/demo/Ursa.Demo/Pages/ElasticWrapPanelDemo.axaml
@@ -64,6 +64,22 @@
Minimum="0"
Value="{Binding ItemHeight}" />
+
+
+
+
+
+
@@ -108,6 +124,8 @@
IsFillVertical="{Binding IsFillVertical}"
ItemHeight="{Binding ItemHeight}"
ItemWidth="{Binding ItemWidth}"
+ ItemSpacing="{Binding ItemSpacing}"
+ LineSpacing="{Binding LineSpacing}"
Orientation="{Binding SelectedOrientation}">
@@ -134,6 +152,8 @@
IsFillVertical="{Binding IsFillVertical}"
ItemHeight="{Binding ItemHeight}"
ItemWidth="{Binding ItemWidth}"
+ ItemSpacing="{Binding ItemSpacing}"
+ LineSpacing="{Binding LineSpacing}"
Orientation="{Binding SelectedOrientation}">
@@ -165,6 +185,8 @@
IsFillVertical="{Binding IsFillVertical}"
ItemHeight="{Binding ItemHeight}"
ItemWidth="{Binding ItemWidth}"
+ ItemSpacing="{Binding ItemSpacing}"
+ LineSpacing="{Binding LineSpacing}"
Orientation="{Binding SelectedOrientation}">
@@ -200,6 +222,8 @@
@@ -223,6 +247,8 @@
IsFillVertical="{Binding IsFillVertical}"
ItemHeight="{Binding ItemHeight}"
ItemWidth="{Binding ItemWidth}"
+ ItemSpacing="{Binding ItemSpacing}"
+ LineSpacing="{Binding LineSpacing}"
Orientation="{Binding SelectedOrientation}">
@@ -249,6 +275,8 @@
@@ -285,6 +313,8 @@
IsFillVertical="{Binding IsFillVertical}"
ItemHeight="{Binding ItemHeight}"
ItemWidth="{Binding ItemWidth}"
+ ItemSpacing="{Binding ItemSpacing}"
+ LineSpacing="{Binding LineSpacing}"
Orientation="{Binding SelectedOrientation}">
diff --git a/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs
index 4964f36..a3f0b2e 100644
--- a/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/ElasticWrapPanelDemoViewModel.cs
@@ -14,6 +14,8 @@ public partial class ElasticWrapPanelDemoViewModel : ObservableObject
[ObservableProperty] private bool _isFillVertical;
[ObservableProperty] private double _itemWidth = 40d;
[ObservableProperty] private double _itemHeight = 40d;
+ [ObservableProperty] private double _itemSpacing;
+ [ObservableProperty] private double _lineSpacing;
[ObservableProperty] private bool _autoWidth = true;
[ObservableProperty] private bool _autoHeight = true;
diff --git a/src/Ursa/Controls/Panels/ElasticWrapPanel.cs b/src/Ursa/Controls/Panels/ElasticWrapPanel.cs
index ac603a5..87751af 100644
--- a/src/Ursa/Controls/Panels/ElasticWrapPanel.cs
+++ b/src/Ursa/Controls/Panels/ElasticWrapPanel.cs
@@ -12,8 +12,8 @@ public class ElasticWrapPanel : WrapPanel
{
static ElasticWrapPanel()
{
- AffectsMeasure(IsFillHorizontalProperty, IsFillVerticalProperty);
- AffectsArrange(IsFillHorizontalProperty, IsFillVerticalProperty);
+ AffectsMeasure(IsFillHorizontalProperty, IsFillVerticalProperty, ItemSpacingProperty, LineSpacingProperty);
+ AffectsArrange(IsFillHorizontalProperty, IsFillVerticalProperty, ItemSpacingProperty, LineSpacingProperty);
}
#region AttachedProperty
@@ -78,6 +78,8 @@ public class ElasticWrapPanel : WrapPanel
double itemHeight = ItemHeight;
var orientation = Orientation;
var children = Children;
+ double itemSpacing = ItemSpacing;
+ double lineSpacing = LineSpacing;
// Determine the space required for items in the same row/column based on horizontal/vertical arrangement
var curLineSize = new UVSize(orientation);
@@ -111,6 +113,8 @@ public class ElasticWrapPanel : WrapPanel
itemWidthSet ? itemWidth : 0,
itemHeightSet ? itemHeight : 0);
+ bool isFirstItemInLine = true;
+
foreach (var child in children)
{
UVSize sz;
@@ -138,21 +142,38 @@ public class ElasticWrapPanel : WrapPanel
sz.V = itemSetSize.V;
}
- if (MathHelpers.GreaterThan(curLineSize.U + sz.U, uvConstraint.U))
+ double itemUWithSpacing = isFirstItemInLine ? sz.U : sz.U + itemSpacing;
+
+ if (MathHelpers.GreaterThan(curLineSize.U + itemUWithSpacing, uvConstraint.U))
{
panelSize.U = Max(curLineSize.U, panelSize.U);
+ if (panelSize.V > 0)
+ {
+ panelSize.V += lineSpacing;
+ }
panelSize.V += curLineSize.V;
curLineSize = sz;
+ isFirstItemInLine = false;
}
else
{
+ if (!isFirstItemInLine)
+ {
+ curLineSize.U += itemSpacing;
+ }
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
panelSize.U = Max(curLineSize.U, panelSize.U);
+ if (panelSize.V > 0)
+ {
+ panelSize.V += lineSpacing;
+ }
panelSize.V += curLineSize.V;
+ isFirstItemInLine = false;
}
curLineSize = new UVSize(orientation);
+ isFirstItemInLine = true;
}
else
{
@@ -164,23 +185,40 @@ public class ElasticWrapPanel : WrapPanel
itemWidthSet ? itemWidth : child.DesiredSize.Width,
itemHeightSet ? itemHeight : child.DesiredSize.Height);
- if (MathHelpers.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) // Need to switch to another line
+ double itemUWithSpacing = isFirstItemInLine ? sz.U : sz.U + itemSpacing;
+
+ if (MathHelpers.GreaterThan(curLineSize.U + itemUWithSpacing, uvConstraint.U)) // Need to switch to another line
{
panelSize.U = Max(curLineSize.U, panelSize.U);
+ if (panelSize.V > 0)
+ {
+ panelSize.V += lineSpacing;
+ }
panelSize.V += curLineSize.V;
curLineSize = sz;
+ isFirstItemInLine = false;
if (MathHelpers.GreaterThan(sz.U, uvConstraint.U)) // The element is wider than the constraint - give it a separate line
{
panelSize.U = Max(sz.U, panelSize.U);
+ if (panelSize.V > 0)
+ {
+ panelSize.V += lineSpacing;
+ }
panelSize.V += sz.V;
curLineSize = new UVSize(orientation);
+ isFirstItemInLine = true;
}
}
else // Continue to accumulate a line
{
+ if (!isFirstItemInLine)
+ {
+ curLineSize.U += itemSpacing;
+ }
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
+ isFirstItemInLine = false;
}
}
}
@@ -197,6 +235,8 @@ public class ElasticWrapPanel : WrapPanel
{
bool itemWidthSet = !double.IsNaN(ItemWidth);
bool itemHeightSet = !double.IsNaN(ItemHeight);
+ double itemSpacing = ItemSpacing;
+ double lineSpacing = LineSpacing;
// This is the size for non-space measurement
UVSize itemSetSize = new UVSize(Orientation,
@@ -212,7 +252,7 @@ public class ElasticWrapPanel : WrapPanel
#region Get the collection of elements in the same direction
// Current collection of elements in a row/column
- UVCollection curLineUIs = new UVCollection(Orientation, itemSetSize);
+ UVCollection curLineUIs = new UVCollection(Orientation, itemSetSize, itemSpacing);
// Iterate over the child elements
var children = Children;
@@ -242,14 +282,16 @@ public class ElasticWrapPanel : WrapPanel
sz.V = itemSetSize.V;
}
- if (MathHelpers.GreaterThan(curLineUIs.TotalU + sz.U, uvFinalSize.U))
+ // Account for spacing before this item if it's not the first
+ double spacingBeforeItem = curLineUIs.Count > 0 ? itemSpacing : 0;
+ if (MathHelpers.GreaterThan(curLineUIs.TotalU + spacingBeforeItem + sz.U, uvFinalSize.U))
{
if (curLineUIs.Count > 0)
{
lineUVCollection.Add(curLineUIs);
}
- curLineUIs = new UVCollection(Orientation, itemSetSize);
+ curLineUIs = new UVCollection(Orientation, itemSetSize, itemSpacing);
curLineUIs.Add(child, sz, Convert.ToInt32(lengthCount));
}
else
@@ -258,7 +300,7 @@ public class ElasticWrapPanel : WrapPanel
}
lineUVCollection.Add(curLineUIs);
- curLineUIs = new UVCollection(Orientation, itemSetSize);
+ curLineUIs = new UVCollection(Orientation, itemSetSize, itemSpacing);
}
else
{
@@ -266,19 +308,21 @@ public class ElasticWrapPanel : WrapPanel
itemWidthSet ? ItemWidth : child.DesiredSize.Width,
itemHeightSet ? ItemHeight : child.DesiredSize.Height);
- if (MathHelpers.GreaterThan(curLineUIs.TotalU + sz.U, uvFinalSize.U)) // Need to switch to another line
+ // Account for spacing before this item if it's not the first
+ double spacingBeforeItem = curLineUIs.Count > 0 ? itemSpacing : 0;
+ if (MathHelpers.GreaterThan(curLineUIs.TotalU + spacingBeforeItem + sz.U, uvFinalSize.U)) // Need to switch to another line
{
if (curLineUIs.Count > 0)
{
lineUVCollection.Add(curLineUIs);
}
- curLineUIs = new UVCollection(Orientation, itemSetSize);
+ curLineUIs = new UVCollection(Orientation, itemSetSize, itemSpacing);
curLineUIs.Add(child, sz);
if (MathHelpers.GreaterThan(sz.U, uvFinalSize.U))
{
lineUVCollection.Add(curLineUIs);
- curLineUIs = new UVCollection(Orientation, itemSetSize);
+ curLineUIs = new UVCollection(Orientation, itemSetSize, itemSpacing);
}
}
else
@@ -332,7 +376,8 @@ public class ElasticWrapPanel : WrapPanel
}
maxElementCount = Max(sum, maxElementCount);
}
- adaptULength = (uvFinalSize.U - maxElementCount * itemSetSize.U) / maxElementCount;
+ double totalItemSpacing = maxElementCount > 1 ? (maxElementCount - 1) * itemSpacing : 0;
+ adaptULength = (uvFinalSize.U - maxElementCount * itemSetSize.U - totalItemSpacing) / maxElementCount;
adaptULength = Max(adaptULength, 0);
}
}
@@ -342,16 +387,19 @@ public class ElasticWrapPanel : WrapPanel
if (itemSetSize.V > 0)
{
isAdaptV = true;
- adaptVLength = uvFinalSize.V / lineUVCollection.Count;
+ double totalLineSpacing = lineUVCollection.Count > 1 ? (lineUVCollection.Count - 1) * lineSpacing : 0;
+ adaptVLength = (uvFinalSize.V - totalLineSpacing) / lineUVCollection.Count;
}
}
bool isHorizontal = Orientation == Orientation.Horizontal;
+ int lineIndex = 0;
foreach (var uvCollection in lineUVCollection)
{
double u = 0;
var lineUIEles = uvCollection.UICollection.Keys.ToList();
double linevV = isAdaptV ? adaptVLength : uvCollection.LineV;
+ int itemIndex = 0;
foreach (var child in lineUIEles)
{
UVLengthSize childSize = uvCollection.UICollection[child];
@@ -384,9 +432,19 @@ public class ElasticWrapPanel : WrapPanel
}
u += layoutSlotU;
+ if (itemIndex < lineUIEles.Count - 1)
+ {
+ u += itemSpacing;
+ }
+ itemIndex++;
}
accumulatedV += linevV;
+ if (lineIndex < lineUVCollection.Count - 1)
+ {
+ accumulatedV += lineSpacing;
+ }
+ lineIndex++;
lineUIEles.Clear();
}
}
@@ -462,15 +520,26 @@ public class ElasticWrapPanel : WrapPanel
private UVSize LineDesireUVSize;
private UVSize ItemSetSize;
+
+ private double ItemSpacing;
- public UVCollection(Orientation orientation, UVSize itemSetSize)
+ public UVCollection(Orientation orientation, UVSize itemSetSize, double itemSpacing = 0)
{
UICollection = new Dictionary();
LineDesireUVSize = new UVSize(orientation);
ItemSetSize = itemSetSize;
+ ItemSpacing = itemSpacing;
}
- public double TotalU => LineDesireUVSize.U;
+ public double TotalU
+ {
+ get
+ {
+ // TotalU includes spacing between items (not before first or after last)
+ double spacingTotal = UICollection.Count > 1 ? (UICollection.Count - 1) * ItemSpacing : 0;
+ return LineDesireUVSize.U + spacingTotal;
+ }
+ }
public double LineV => LineDesireUVSize.V;
diff --git a/tests/HeadlessTest.Ursa/Controls/ElasticWrapPanelTests/Tests.cs b/tests/HeadlessTest.Ursa/Controls/ElasticWrapPanelTests/Tests.cs
index 8b20e87..0069399 100644
--- a/tests/HeadlessTest.Ursa/Controls/ElasticWrapPanelTests/Tests.cs
+++ b/tests/HeadlessTest.Ursa/Controls/ElasticWrapPanelTests/Tests.cs
@@ -69,4 +69,151 @@ public class Tests
window.Show();
Assert.Equal(2, panel.LineCount);
}
+
+ [AvaloniaFact]
+ public void ItemSpacing_Applied_Correctly()
+ {
+ var window = new Window() { };
+ var panel = new ElasticWrapPanel
+ {
+ Width = 400,
+ Height = 200,
+ Orientation = Orientation.Horizontal,
+ ItemSpacing = 10,
+ };
+
+ // Add 3 rectangles of 100x100 with 10px spacing
+ // Total width should be: 100 + 10 + 100 + 10 + 100 = 320
+ for (int i = 0; i < 3; i++)
+ {
+ var rect = new Rectangle
+ {
+ Width = 100,
+ Height = 100,
+ };
+ panel.Children.Add(rect);
+ }
+
+ window.Content = panel;
+ window.Show();
+
+ // All 3 should fit on one line
+ Assert.Equal(1, panel.LineCount);
+
+ // Check that the desired size accounts for spacing
+ Assert.True(panel.DesiredSize.Width >= 320);
+ }
+
+ [AvaloniaFact]
+ public void LineSpacing_Applied_Correctly()
+ {
+ var window = new Window() { };
+ var panel = new ElasticWrapPanel
+ {
+ Width = 200,
+ Height = 400,
+ Orientation = Orientation.Horizontal,
+ LineSpacing = 20,
+ };
+
+ // Add 4 rectangles of 100x100, should wrap to 2 lines
+ for (int i = 0; i < 4; i++)
+ {
+ var rect = new Rectangle
+ {
+ Width = 100,
+ Height = 100,
+ };
+ panel.Children.Add(rect);
+ }
+
+ window.Content = panel;
+ window.Show();
+
+ // Should have 2 lines
+ Assert.Equal(2, panel.LineCount);
+
+ // Total height should be: 100 + 20 + 100 = 220
+ Assert.True(panel.DesiredSize.Height >= 220);
+ }
+
+ [AvaloniaFact]
+ public void ItemSpacing_And_LineSpacing_Together()
+ {
+ var window = new Window() { };
+ var panel = new ElasticWrapPanel
+ {
+ Width = 250,
+ Height = 400,
+ Orientation = Orientation.Horizontal,
+ ItemSpacing = 10,
+ LineSpacing = 20,
+ };
+
+ // Add 4 rectangles of 100x100
+ // With ItemSpacing=10, two items per line need 100 + 10 + 100 = 210 width
+ // With LineSpacing=20, two lines need 100 + 20 + 100 = 220 height
+ for (int i = 0; i < 4; i++)
+ {
+ var rect = new Rectangle
+ {
+ Width = 100,
+ Height = 100,
+ };
+ panel.Children.Add(rect);
+ }
+
+ window.Content = panel;
+ window.Show();
+
+ // Should have 2 lines
+ Assert.Equal(2, panel.LineCount);
+
+ // Check dimensions include spacing
+ Assert.True(panel.DesiredSize.Width >= 210);
+ Assert.True(panel.DesiredSize.Height >= 220);
+ }
+
+ [AvaloniaFact]
+ public void ItemSpacing_Does_Not_Affect_LineCount()
+ {
+ var window = new Window() { };
+
+ // Panel without spacing - should fit 2 items per line (200 width / 100 per item = 2)
+ var panel1 = new ElasticWrapPanel
+ {
+ Width = 200,
+ Height = 400,
+ Orientation = Orientation.Horizontal,
+ ItemSpacing = 0,
+ };
+
+ for (int i = 0; i < 4; i++)
+ {
+ panel1.Children.Add(new Rectangle { Width = 100, Height = 100 });
+ }
+
+ // Panel with spacing - items with spacing should fit fewer per line
+ // 100 + 10 + 100 = 210 > 200, so only 1 item per line
+ var panel2 = new ElasticWrapPanel
+ {
+ Width = 200,
+ Height = 400,
+ Orientation = Orientation.Horizontal,
+ ItemSpacing = 10,
+ };
+
+ for (int i = 0; i < 4; i++)
+ {
+ panel2.Children.Add(new Rectangle { Width = 100, Height = 100 });
+ }
+
+ window.Content = panel1;
+ window.Show();
+ Assert.Equal(2, panel1.LineCount); // 2 items per line, 4 items total = 2 lines
+
+ window.Content = panel2;
+ window.UpdateLayout();
+ Assert.Equal(4, panel2.LineCount); // 1 item per line, 4 items total = 4 lines
+ }
}
\ No newline at end of file