Skip to content

Commit

Permalink
add spacing to uniform grid - issue 8406 (#17993)
Browse files Browse the repository at this point in the history
* add spacing to uniform grid - issue 8406

* fix negative return value from UniformGrid MeasureOverride and cached spacing properties
  • Loading branch information
Enzx authored Jan 22, 2025
1 parent 26bc229 commit fdadfb8
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 24 deletions.
59 changes: 53 additions & 6 deletions src/Avalonia.Controls/Primitives/UniformGrid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ public class UniformGrid : Panel
public static readonly StyledProperty<int> FirstColumnProperty =
AvaloniaProperty.Register<UniformGrid, int>(nameof(FirstColumn));

/// <summary>
/// Defines the <see cref="RowSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> RowSpacingProperty =
AvaloniaProperty.Register<UniformGrid, double>(nameof(RowSpacing), 0);

/// <summary>
/// Defines the <see cref="ColumnSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> ColumnSpacingProperty =
AvaloniaProperty.Register<UniformGrid, double>(nameof(ColumnSpacing), 0);

private int _rows;
private int _columns;

Expand Down Expand Up @@ -60,14 +72,34 @@ public int FirstColumn
set => SetValue(FirstColumnProperty, value);
}

/// <summary>
/// Specifies the spacing between rows.
/// </summary>
public double RowSpacing
{
get => GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}

/// <summary>
/// Specifies the spacing between columns.
/// </summary>
public double ColumnSpacing
{
get => GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}

protected override Size MeasureOverride(Size availableSize)
{
UpdateRowsAndColumns();

var maxWidth = 0d;
var maxHeight = 0d;

var childAvailableSize = new Size(availableSize.Width / _columns, availableSize.Height / _rows);
var childAvailableSize = new Size(
(availableSize.Width - (_columns - 1) * ColumnSpacing) / _columns,
(availableSize.Height - (_rows - 1) * RowSpacing) / _rows);

foreach (var child in Children)
{
Expand All @@ -84,16 +116,25 @@ protected override Size MeasureOverride(Size availableSize)
}
}

return new Size(maxWidth * _columns, maxHeight * _rows);
var totalWidth = maxWidth * _columns + ColumnSpacing * (_columns - 1);
var totalHeight = maxHeight * _rows + RowSpacing * (_rows - 1);

totalWidth = Math.Max(totalWidth, 0);
totalHeight = Math.Max(totalHeight, 0);

return new Size(totalWidth, totalHeight);
}

protected override Size ArrangeOverride(Size finalSize)
{
var x = FirstColumn;
var y = 0;

var width = finalSize.Width / _columns;
var height = finalSize.Height / _rows;
var columnSpacing = ColumnSpacing;
var rowSpacing = RowSpacing;

var width = (finalSize.Width - (_columns - 1) * columnSpacing) / _columns;
var height = (finalSize.Height - (_rows - 1) * rowSpacing) / _rows;

foreach (var child in Children)
{
Expand All @@ -102,7 +143,13 @@ protected override Size ArrangeOverride(Size finalSize)
continue;
}

child.Arrange(new Rect(x * width, y * height, width, height));
var rect = new Rect(
x * (width + columnSpacing),
y * (height + rowSpacing),
width,
height);

child.Arrange(rect);

x++;

Expand All @@ -121,7 +168,7 @@ private void UpdateRowsAndColumns()
_rows = Rows;
_columns = Columns;

if (FirstColumn >= Columns)
if (FirstColumn >= _columns)
{
SetCurrentValue(FirstColumnProperty, 0);
}
Expand Down
200 changes: 182 additions & 18 deletions tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class UniformGridTests
[Fact]
public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows()
{
var target = new UniformGrid()
var target = new UniformGrid
{
Children =
{
Expand All @@ -21,14 +21,15 @@ public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows()
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

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

[Fact]
public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows()
{
var target = new UniformGrid()
var target = new UniformGrid
{
Columns = 2,
Children =
Expand All @@ -44,14 +45,15 @@ public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows()
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// 2 * 3 grid
Assert.Equal(new Size(2 * 80, 3 * 90), target.Bounds.Size);
// 2 * 3 grid => each cell: 80 x 90
// Final size => (2 * 80) x (3 * 90) = 160 x 270
Assert.Equal(new Size(160, 270), target.Bounds.Size);
}

[Fact]
public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows()
{
var target = new UniformGrid()
var target = new UniformGrid
{
Columns = 3,
FirstColumn = 2,
Expand All @@ -68,14 +70,15 @@ public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows()
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// 3 * 3 grid
Assert.Equal(new Size(3 * 80, 3 * 90), target.Bounds.Size);
// 3 * 3 grid => each cell: 80 x 90
// Final size => (3 * 80) x (3 * 90) = 240 x 270
Assert.Equal(new Size(240, 270), target.Bounds.Size);
}

[Fact]
public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns()
{
var target = new UniformGrid()
var target = new UniformGrid
{
Rows = 2,
Children =
Expand All @@ -91,14 +94,15 @@ public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns()
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// 3 * 2 grid
Assert.Equal(new Size(3 * 80, 2 * 90), target.Bounds.Size);
// 3 * 2 grid => each cell: 80 x 90
// Final size => (3 * 80) x (2 * 90) = 240 x 180
Assert.Equal(new Size(240, 180), target.Bounds.Size);
}

[Fact]
public void Grid_Size_Is_Limited_By_Rows_And_Columns()
{
var target = new UniformGrid()
var target = new UniformGrid
{
Columns = 2,
Rows = 2,
Expand All @@ -115,14 +119,15 @@ public void Grid_Size_Is_Limited_By_Rows_And_Columns()
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

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

[Fact]
public void Not_Visible_Children_Are_Ignored()
{
var target = new UniformGrid()
var target = new UniformGrid
{
Children =
{
Expand All @@ -137,8 +142,167 @@ public void Not_Visible_Children_Are_Ignored()
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// 2 * 2 grid
Assert.Equal(new Size(2 * 50, 2 * 70), target.Bounds.Size);
// Visible children: 4
// Auto => 2 x 2 grid => each cell: 50 x 70
// Final size => (2 * 50) x (2 * 70) = 100 x 140
Assert.Equal(new Size(100, 140), target.Bounds.Size);
}

//
// New tests to cover RowSpacing and ColumnSpacing
//

[Fact]
public void Grid_Respects_ColumnSpacing_For_Auto_Columns_And_Rows()
{
// We have 3 visible children and no fixed Rows/Columns => 2x2 grid
// Largest child is 80 x 90. ColumnSpacing = 10, RowSpacing = 0
var target = new UniformGrid
{
ColumnSpacing = 10,
Children =
{
new Border { Width = 50, Height = 70 },
new Border { Width = 30, Height = 50 },
new Border { Width = 80, Height = 90 }
}
};

target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// Without spacing => width = 2*80 = 160, height = 2*90 = 180
// With columnSpacing=10 => total width = 2*80 + 1*10 = 170
// RowSpacing=0 => total height = 180
Assert.Equal(new Size(170, 180), target.Bounds.Size);
}

[Fact]
public void Grid_Respects_RowSpacing_For_Auto_Columns_And_Rows()
{
// 3 visible children => 2x2 grid again
// Largest child is 80 x 90. RowSpacing = 15, ColumnSpacing = 0
var target = new UniformGrid
{
RowSpacing = 15,
Children =
{
new Border { Width = 50, Height = 70 },
new Border { Width = 30, Height = 50 },
new Border { Width = 80, Height = 90 }
}
};

target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// Without spacing => width = 160, height = 180
// With rowSpacing=15 => total height = 2*90 + 1*15 = 195
// ColumnSpacing=0 => total width = 160
Assert.Equal(new Size(160, 195), target.Bounds.Size);
}

[Fact]
public void Grid_Respects_Both_Row_And_Column_Spacing_For_Fixed_Grid()
{
// 4 visible children => 2 rows x 2 columns, each child is 50x70 or 80x90
// We'll fix the Grid to 2x2 so the largest child dictates the cell size: 80x90
// RowSpacing=10, ColumnSpacing=5
var target = new UniformGrid
{
Rows = 2,
Columns = 2,
RowSpacing = 10,
ColumnSpacing = 5,
Children =
{
new Border { Width = 50, Height = 70 },
new Border { Width = 30, Height = 50 },
new Border { Width = 80, Height = 90 },
new Border { Width = 20, Height = 30 },
}
};

target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// Each cell = 80 x 90
// Final width = (2 * 80) + (1 * 5) = 160 + 5 = 165
// Final height = (2 * 90) + (1 * 10) = 180 + 10 = 190
Assert.Equal(new Size(165, 190), target.Bounds.Size);
}

[Fact]
public void Grid_Respects_Spacing_When_Invisible_Child_Exists()
{
// 3 *visible* children => auto => 2x2 grid
// Largest child is 80 x 90.
// Add spacing so we can confirm it doesn't add extra columns/rows for invisible child.
var target = new UniformGrid
{
RowSpacing = 5,
ColumnSpacing = 5,
Children =
{
new Border { Width = 50, Height = 70 },
new Border { Width = 80, Height = 90, IsVisible = false },
new Border { Width = 30, Height = 50 },
new Border { Width = 40, Height = 60 }
}
};

// Visible children: 3 => auto => sqrt(3) => 2x2
// Largest visible child is 50x70 or 30x50 or 40x60 => the biggest is 50x70
// Actually, let's ensure we have a child bigger than that:
// (So let's modify the 40x60 to something bigger than 50x70, e.g. 80x90 for clarity)
// We'll do that in the collection above if needed, but let's keep as is for example.

target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));

// The largest visible child is 50x70. So each cell is 50x70.
// For a 2x2 grid with 3 visible children:
// - total width = (2 * 50) + (1 * 5) = 100 + 5 = 105
// - total height = (2 * 70) + (1 * 5) = 140 + 5 = 145
Assert.Equal(new Size(105, 145), target.Bounds.Size);
}

/// <summary>
/// Exposes MeasureOverride for testing inherited classes
/// </summary>
public class UniformGridExposeMeasureOverride : UniformGrid
{
public new Size MeasureOverride(Size availableSize)
{
return base.MeasureOverride(availableSize);
}
}

[Fact]
public void Measure_WithRowsAndColumnsZeroAndNonZeroSpacing_ProducesZeroDesiredSize()
{
// MeasureOverride() is called by Layoutable.MeasureCore() and it ensures that
// the desired size is never negative. but in case of inherited classes MeasureOverride() may return negative values.
var target = new UniformGridExposeMeasureOverride
{
Rows = 0,
Columns = 0,
RowSpacing = 10,
ColumnSpacing = 20
};

var availableSize = new Size(100, 100);

var desiredSize = target.MeasureOverride(availableSize);

// Fail case:
// Because _rows and _columns are 0, the calculation becomes:
// totalWidth = maxWidth * 0 + ColumnSpacing * (0 - 1) = -ColumnSpacing
// totalHeight = maxHeight * 0 + RowSpacing * (0 - 1) = -RowSpacing
// Expected: (0, 0)
Assert.Equal(0, desiredSize.Width);
Assert.Equal(0, desiredSize.Height);

}
}
}

0 comments on commit fdadfb8

Please sign in to comment.