Skip to content

Commit fdadfb8

Browse files
authored
add spacing to uniform grid - issue 8406 (#17993)
* add spacing to uniform grid - issue 8406 * fix negative return value from UniformGrid MeasureOverride and cached spacing properties
1 parent 26bc229 commit fdadfb8

File tree

2 files changed

+235
-24
lines changed

2 files changed

+235
-24
lines changed

src/Avalonia.Controls/Primitives/UniformGrid.cs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ public class UniformGrid : Panel
2525
public static readonly StyledProperty<int> FirstColumnProperty =
2626
AvaloniaProperty.Register<UniformGrid, int>(nameof(FirstColumn));
2727

28+
/// <summary>
29+
/// Defines the <see cref="RowSpacing"/> property.
30+
/// </summary>
31+
public static readonly StyledProperty<double> RowSpacingProperty =
32+
AvaloniaProperty.Register<UniformGrid, double>(nameof(RowSpacing), 0);
33+
34+
/// <summary>
35+
/// Defines the <see cref="ColumnSpacing"/> property.
36+
/// </summary>
37+
public static readonly StyledProperty<double> ColumnSpacingProperty =
38+
AvaloniaProperty.Register<UniformGrid, double>(nameof(ColumnSpacing), 0);
39+
2840
private int _rows;
2941
private int _columns;
3042

@@ -60,14 +72,34 @@ public int FirstColumn
6072
set => SetValue(FirstColumnProperty, value);
6173
}
6274

75+
/// <summary>
76+
/// Specifies the spacing between rows.
77+
/// </summary>
78+
public double RowSpacing
79+
{
80+
get => GetValue(RowSpacingProperty);
81+
set => SetValue(RowSpacingProperty, value);
82+
}
83+
84+
/// <summary>
85+
/// Specifies the spacing between columns.
86+
/// </summary>
87+
public double ColumnSpacing
88+
{
89+
get => GetValue(ColumnSpacingProperty);
90+
set => SetValue(ColumnSpacingProperty, value);
91+
}
92+
6393
protected override Size MeasureOverride(Size availableSize)
6494
{
6595
UpdateRowsAndColumns();
6696

6797
var maxWidth = 0d;
6898
var maxHeight = 0d;
6999

70-
var childAvailableSize = new Size(availableSize.Width / _columns, availableSize.Height / _rows);
100+
var childAvailableSize = new Size(
101+
(availableSize.Width - (_columns - 1) * ColumnSpacing) / _columns,
102+
(availableSize.Height - (_rows - 1) * RowSpacing) / _rows);
71103

72104
foreach (var child in Children)
73105
{
@@ -84,16 +116,25 @@ protected override Size MeasureOverride(Size availableSize)
84116
}
85117
}
86118

87-
return new Size(maxWidth * _columns, maxHeight * _rows);
119+
var totalWidth = maxWidth * _columns + ColumnSpacing * (_columns - 1);
120+
var totalHeight = maxHeight * _rows + RowSpacing * (_rows - 1);
121+
122+
totalWidth = Math.Max(totalWidth, 0);
123+
totalHeight = Math.Max(totalHeight, 0);
124+
125+
return new Size(totalWidth, totalHeight);
88126
}
89127

90128
protected override Size ArrangeOverride(Size finalSize)
91129
{
92130
var x = FirstColumn;
93131
var y = 0;
94132

95-
var width = finalSize.Width / _columns;
96-
var height = finalSize.Height / _rows;
133+
var columnSpacing = ColumnSpacing;
134+
var rowSpacing = RowSpacing;
135+
136+
var width = (finalSize.Width - (_columns - 1) * columnSpacing) / _columns;
137+
var height = (finalSize.Height - (_rows - 1) * rowSpacing) / _rows;
97138

98139
foreach (var child in Children)
99140
{
@@ -102,7 +143,13 @@ protected override Size ArrangeOverride(Size finalSize)
102143
continue;
103144
}
104145

105-
child.Arrange(new Rect(x * width, y * height, width, height));
146+
var rect = new Rect(
147+
x * (width + columnSpacing),
148+
y * (height + rowSpacing),
149+
width,
150+
height);
151+
152+
child.Arrange(rect);
106153

107154
x++;
108155

@@ -121,7 +168,7 @@ private void UpdateRowsAndColumns()
121168
_rows = Rows;
122169
_columns = Columns;
123170

124-
if (FirstColumn >= Columns)
171+
if (FirstColumn >= _columns)
125172
{
126173
SetCurrentValue(FirstColumnProperty, 0);
127174
}

tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs

Lines changed: 182 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class UniformGridTests
88
[Fact]
99
public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows()
1010
{
11-
var target = new UniformGrid()
11+
var target = new UniformGrid
1212
{
1313
Children =
1414
{
@@ -21,14 +21,15 @@ public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows()
2121
target.Measure(Size.Infinity);
2222
target.Arrange(new Rect(target.DesiredSize));
2323

24-
// 2 * 2 grid
25-
Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size);
24+
// 2 * 2 grid => each cell: 80 x 90
25+
// Final size => (2 * 80) x (2 * 90) = 160 x 180
26+
Assert.Equal(new Size(160, 180), target.Bounds.Size);
2627
}
2728

2829
[Fact]
2930
public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows()
3031
{
31-
var target = new UniformGrid()
32+
var target = new UniformGrid
3233
{
3334
Columns = 2,
3435
Children =
@@ -44,14 +45,15 @@ public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows()
4445
target.Measure(Size.Infinity);
4546
target.Arrange(new Rect(target.DesiredSize));
4647

47-
// 2 * 3 grid
48-
Assert.Equal(new Size(2 * 80, 3 * 90), target.Bounds.Size);
48+
// 2 * 3 grid => each cell: 80 x 90
49+
// Final size => (2 * 80) x (3 * 90) = 160 x 270
50+
Assert.Equal(new Size(160, 270), target.Bounds.Size);
4951
}
5052

5153
[Fact]
5254
public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows()
5355
{
54-
var target = new UniformGrid()
56+
var target = new UniformGrid
5557
{
5658
Columns = 3,
5759
FirstColumn = 2,
@@ -68,14 +70,15 @@ public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows()
6870
target.Measure(Size.Infinity);
6971
target.Arrange(new Rect(target.DesiredSize));
7072

71-
// 3 * 3 grid
72-
Assert.Equal(new Size(3 * 80, 3 * 90), target.Bounds.Size);
73+
// 3 * 3 grid => each cell: 80 x 90
74+
// Final size => (3 * 80) x (3 * 90) = 240 x 270
75+
Assert.Equal(new Size(240, 270), target.Bounds.Size);
7376
}
7477

7578
[Fact]
7679
public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns()
7780
{
78-
var target = new UniformGrid()
81+
var target = new UniformGrid
7982
{
8083
Rows = 2,
8184
Children =
@@ -91,14 +94,15 @@ public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns()
9194
target.Measure(Size.Infinity);
9295
target.Arrange(new Rect(target.DesiredSize));
9396

94-
// 3 * 2 grid
95-
Assert.Equal(new Size(3 * 80, 2 * 90), target.Bounds.Size);
97+
// 3 * 2 grid => each cell: 80 x 90
98+
// Final size => (3 * 80) x (2 * 90) = 240 x 180
99+
Assert.Equal(new Size(240, 180), target.Bounds.Size);
96100
}
97101

98102
[Fact]
99103
public void Grid_Size_Is_Limited_By_Rows_And_Columns()
100104
{
101-
var target = new UniformGrid()
105+
var target = new UniformGrid
102106
{
103107
Columns = 2,
104108
Rows = 2,
@@ -115,14 +119,15 @@ public void Grid_Size_Is_Limited_By_Rows_And_Columns()
115119
target.Measure(Size.Infinity);
116120
target.Arrange(new Rect(target.DesiredSize));
117121

118-
// 2 * 2 grid
119-
Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size);
122+
// 2 * 2 grid => each cell: 80 x 90
123+
// Final size => (2 * 80) x (2 * 90) = 160 x 180
124+
Assert.Equal(new Size(160, 180), target.Bounds.Size);
120125
}
121126

122127
[Fact]
123128
public void Not_Visible_Children_Are_Ignored()
124129
{
125-
var target = new UniformGrid()
130+
var target = new UniformGrid
126131
{
127132
Children =
128133
{
@@ -137,8 +142,167 @@ public void Not_Visible_Children_Are_Ignored()
137142
target.Measure(Size.Infinity);
138143
target.Arrange(new Rect(target.DesiredSize));
139144

140-
// 2 * 2 grid
141-
Assert.Equal(new Size(2 * 50, 2 * 70), target.Bounds.Size);
145+
// Visible children: 4
146+
// Auto => 2 x 2 grid => each cell: 50 x 70
147+
// Final size => (2 * 50) x (2 * 70) = 100 x 140
148+
Assert.Equal(new Size(100, 140), target.Bounds.Size);
149+
}
150+
151+
//
152+
// New tests to cover RowSpacing and ColumnSpacing
153+
//
154+
155+
[Fact]
156+
public void Grid_Respects_ColumnSpacing_For_Auto_Columns_And_Rows()
157+
{
158+
// We have 3 visible children and no fixed Rows/Columns => 2x2 grid
159+
// Largest child is 80 x 90. ColumnSpacing = 10, RowSpacing = 0
160+
var target = new UniformGrid
161+
{
162+
ColumnSpacing = 10,
163+
Children =
164+
{
165+
new Border { Width = 50, Height = 70 },
166+
new Border { Width = 30, Height = 50 },
167+
new Border { Width = 80, Height = 90 }
168+
}
169+
};
170+
171+
target.Measure(Size.Infinity);
172+
target.Arrange(new Rect(target.DesiredSize));
173+
174+
// Without spacing => width = 2*80 = 160, height = 2*90 = 180
175+
// With columnSpacing=10 => total width = 2*80 + 1*10 = 170
176+
// RowSpacing=0 => total height = 180
177+
Assert.Equal(new Size(170, 180), target.Bounds.Size);
178+
}
179+
180+
[Fact]
181+
public void Grid_Respects_RowSpacing_For_Auto_Columns_And_Rows()
182+
{
183+
// 3 visible children => 2x2 grid again
184+
// Largest child is 80 x 90. RowSpacing = 15, ColumnSpacing = 0
185+
var target = new UniformGrid
186+
{
187+
RowSpacing = 15,
188+
Children =
189+
{
190+
new Border { Width = 50, Height = 70 },
191+
new Border { Width = 30, Height = 50 },
192+
new Border { Width = 80, Height = 90 }
193+
}
194+
};
195+
196+
target.Measure(Size.Infinity);
197+
target.Arrange(new Rect(target.DesiredSize));
198+
199+
// Without spacing => width = 160, height = 180
200+
// With rowSpacing=15 => total height = 2*90 + 1*15 = 195
201+
// ColumnSpacing=0 => total width = 160
202+
Assert.Equal(new Size(160, 195), target.Bounds.Size);
203+
}
204+
205+
[Fact]
206+
public void Grid_Respects_Both_Row_And_Column_Spacing_For_Fixed_Grid()
207+
{
208+
// 4 visible children => 2 rows x 2 columns, each child is 50x70 or 80x90
209+
// We'll fix the Grid to 2x2 so the largest child dictates the cell size: 80x90
210+
// RowSpacing=10, ColumnSpacing=5
211+
var target = new UniformGrid
212+
{
213+
Rows = 2,
214+
Columns = 2,
215+
RowSpacing = 10,
216+
ColumnSpacing = 5,
217+
Children =
218+
{
219+
new Border { Width = 50, Height = 70 },
220+
new Border { Width = 30, Height = 50 },
221+
new Border { Width = 80, Height = 90 },
222+
new Border { Width = 20, Height = 30 },
223+
}
224+
};
225+
226+
target.Measure(Size.Infinity);
227+
target.Arrange(new Rect(target.DesiredSize));
228+
229+
// Each cell = 80 x 90
230+
// Final width = (2 * 80) + (1 * 5) = 160 + 5 = 165
231+
// Final height = (2 * 90) + (1 * 10) = 180 + 10 = 190
232+
Assert.Equal(new Size(165, 190), target.Bounds.Size);
233+
}
234+
235+
[Fact]
236+
public void Grid_Respects_Spacing_When_Invisible_Child_Exists()
237+
{
238+
// 3 *visible* children => auto => 2x2 grid
239+
// Largest child is 80 x 90.
240+
// Add spacing so we can confirm it doesn't add extra columns/rows for invisible child.
241+
var target = new UniformGrid
242+
{
243+
RowSpacing = 5,
244+
ColumnSpacing = 5,
245+
Children =
246+
{
247+
new Border { Width = 50, Height = 70 },
248+
new Border { Width = 80, Height = 90, IsVisible = false },
249+
new Border { Width = 30, Height = 50 },
250+
new Border { Width = 40, Height = 60 }
251+
}
252+
};
253+
254+
// Visible children: 3 => auto => sqrt(3) => 2x2
255+
// Largest visible child is 50x70 or 30x50 or 40x60 => the biggest is 50x70
256+
// Actually, let's ensure we have a child bigger than that:
257+
// (So let's modify the 40x60 to something bigger than 50x70, e.g. 80x90 for clarity)
258+
// We'll do that in the collection above if needed, but let's keep as is for example.
259+
260+
target.Measure(Size.Infinity);
261+
target.Arrange(new Rect(target.DesiredSize));
262+
263+
// The largest visible child is 50x70. So each cell is 50x70.
264+
// For a 2x2 grid with 3 visible children:
265+
// - total width = (2 * 50) + (1 * 5) = 100 + 5 = 105
266+
// - total height = (2 * 70) + (1 * 5) = 140 + 5 = 145
267+
Assert.Equal(new Size(105, 145), target.Bounds.Size);
268+
}
269+
270+
/// <summary>
271+
/// Exposes MeasureOverride for testing inherited classes
272+
/// </summary>
273+
public class UniformGridExposeMeasureOverride : UniformGrid
274+
{
275+
public new Size MeasureOverride(Size availableSize)
276+
{
277+
return base.MeasureOverride(availableSize);
278+
}
279+
}
280+
281+
[Fact]
282+
public void Measure_WithRowsAndColumnsZeroAndNonZeroSpacing_ProducesZeroDesiredSize()
283+
{
284+
// MeasureOverride() is called by Layoutable.MeasureCore() and it ensures that
285+
// the desired size is never negative. but in case of inherited classes MeasureOverride() may return negative values.
286+
var target = new UniformGridExposeMeasureOverride
287+
{
288+
Rows = 0,
289+
Columns = 0,
290+
RowSpacing = 10,
291+
ColumnSpacing = 20
292+
};
293+
294+
var availableSize = new Size(100, 100);
295+
296+
var desiredSize = target.MeasureOverride(availableSize);
297+
298+
// Fail case:
299+
// Because _rows and _columns are 0, the calculation becomes:
300+
// totalWidth = maxWidth * 0 + ColumnSpacing * (0 - 1) = -ColumnSpacing
301+
// totalHeight = maxHeight * 0 + RowSpacing * (0 - 1) = -RowSpacing
302+
// Expected: (0, 0)
303+
Assert.Equal(0, desiredSize.Width);
304+
Assert.Equal(0, desiredSize.Height);
305+
142306
}
143307
}
144308
}

0 commit comments

Comments
 (0)