From fdadfb862f830b6f74d7d3649b7088cb70fd8caf Mon Sep 17 00:00:00 2001 From: Mohamad Iraji Date: Wed, 22 Jan 2025 10:20:33 +0100 Subject: [PATCH] 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 --- .../Primitives/UniformGrid.cs | 59 +++++- .../Primitives/UniformGridTests.cs | 200 ++++++++++++++++-- 2 files changed, 235 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs index fea35d867a3..f21cb20641e 100644 --- a/src/Avalonia.Controls/Primitives/UniformGrid.cs +++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs @@ -25,6 +25,18 @@ public class UniformGrid : Panel public static readonly StyledProperty FirstColumnProperty = AvaloniaProperty.Register(nameof(FirstColumn)); + /// + /// Defines the property. + /// + public static readonly StyledProperty RowSpacingProperty = + AvaloniaProperty.Register(nameof(RowSpacing), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColumnSpacingProperty = + AvaloniaProperty.Register(nameof(ColumnSpacing), 0); + private int _rows; private int _columns; @@ -60,6 +72,24 @@ public int FirstColumn set => SetValue(FirstColumnProperty, value); } + /// + /// Specifies the spacing between rows. + /// + public double RowSpacing + { + get => GetValue(RowSpacingProperty); + set => SetValue(RowSpacingProperty, value); + } + + /// + /// Specifies the spacing between columns. + /// + public double ColumnSpacing + { + get => GetValue(ColumnSpacingProperty); + set => SetValue(ColumnSpacingProperty, value); + } + protected override Size MeasureOverride(Size availableSize) { UpdateRowsAndColumns(); @@ -67,7 +97,9 @@ protected override Size MeasureOverride(Size availableSize) 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) { @@ -84,7 +116,13 @@ 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) @@ -92,8 +130,11 @@ 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) { @@ -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++; @@ -121,7 +168,7 @@ private void UpdateRowsAndColumns() _rows = Rows; _columns = Columns; - if (FirstColumn >= Columns) + if (FirstColumn >= _columns) { SetCurrentValue(FirstColumnProperty, 0); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs index 340bd09611e..d5cc450587f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs @@ -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 = { @@ -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 = @@ -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, @@ -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 = @@ -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, @@ -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 = { @@ -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); + } + + /// + /// Exposes MeasureOverride for testing inherited classes + /// + 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); + } } }