diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 2e745cd364..0a55a44ad2 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -25,6 +25,8 @@ namespace Avalonia.Controls static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + ColumnSpacingProperty.Changed.AddClassHandler(OnSpacingPropertyChanged); + RowSpacingProperty.Changed.AddClassHandler(OnSpacingPropertyChanged); IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); @@ -32,6 +34,7 @@ namespace Avalonia.Controls RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + AffectsMeasure(ColumnSpacingProperty, RowSpacingProperty); AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); } @@ -161,6 +164,24 @@ namespace Avalonia.Controls set => SetValue(ShowGridLinesProperty, value); } + /// + /// Gets or sets the size of the spacing to place between grid rows. + /// + public double RowSpacing + { + get => GetValue(RowSpacingProperty); + set => SetValue(RowSpacingProperty, value); + } + + /// + /// Gets or sets the size of the spacing to place between grid columns. + /// + public double ColumnSpacing + { + get => GetValue(ColumnSpacingProperty); + set => SetValue(ColumnSpacingProperty, value); + } + /// /// Returns a ColumnDefinitions of column definitions. /// @@ -299,7 +320,7 @@ namespace Avalonia.Controls // the cells belonging to them. // // However, there are cases when topology of a grid causes cyclical - // size dependences. For example: + // size dependencies. For example: // // // column width="Auto" column width="*" @@ -425,17 +446,19 @@ namespace Avalonia.Controls // MeasureCellsGroup(extData.CellGroup1, constraint, false, false); - + double combinedRowSpacing = RowSpacing * (RowDefinitions.Count - 1); + double combinedColumnSpacing = ColumnSpacing * (ColumnDefinitions.Count - 1); + Size innerAvailableSize = new Size(constraint.Width - combinedRowSpacing, constraint.Height - combinedColumnSpacing); { // after Group1 is measured, only Group3 may have cells belonging to Auto rows. bool canResolveStarsV = !HasGroup3CellsInAutoRows; if (canResolveStarsV) { - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(extData.CellGroup2, constraint, false, false); - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); } + MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); } + MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false); } else { @@ -444,9 +467,9 @@ namespace Avalonia.Controls bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length; if (canResolveStarsU) { - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); } + MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false); + if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); } } else { @@ -462,7 +485,7 @@ namespace Avalonia.Controls double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true); - MeasureCellsGroup(extData.CellGroup2, constraint, false, true); + MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, true); do { @@ -472,14 +495,14 @@ namespace Avalonia.Controls ApplyCachedMinSizes(group3MinSizes, true); } - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); } + MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false); // Reset cached Group2Widths ApplyCachedMinSizes(group2MinSizes, false); - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); } + MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); } while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); } @@ -489,8 +512,8 @@ namespace Avalonia.Controls MeasureCellsGroup(extData.CellGroup4, constraint, false, false); gridDesiredSize = new Size( - CalculateDesiredSize(DefinitionsU), - CalculateDesiredSize(DefinitionsV)); + CalculateDesiredSize(DefinitionsU) + ColumnSpacing * (DefinitionsU.Count - 1), + CalculateDesiredSize(DefinitionsV) + RowSpacing * (DefinitionsU.Count - 1)); } } finally @@ -524,9 +547,12 @@ namespace Avalonia.Controls else { Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0); - - SetFinalSize(DefinitionsU, arrangeSize.Width, true); - SetFinalSize(DefinitionsV, arrangeSize.Height, false); + double columnSpacing = ColumnSpacing; + double rowSpacing = RowSpacing; + double combinedRowSpacing = rowSpacing * (RowDefinitions.Count - 1); + double combinedColumnSpacing = columnSpacing * (ColumnDefinitions.Count - 1); + SetFinalSize(DefinitionsU, arrangeSize.Width - combinedColumnSpacing, true); + SetFinalSize(DefinitionsV, arrangeSize.Height - combinedRowSpacing, false); var children = Children; @@ -540,14 +566,13 @@ namespace Avalonia.Controls int rowSpan = PrivateCells[currentCell].RowSpan; Rect cellRect = new Rect( - columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, - rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, + columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset + (columnSpacing * columnIndex), + rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset + (rowSpacing * rowIndex), GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); cell.Arrange(cellRect); - } // update render bound on grid lines renderer visual @@ -2088,7 +2113,7 @@ namespace Avalonia.Controls // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; double[] roundingErrors = RoundingErrors; - double roundedTakenSize = 0.0; + double roundedTakenSize = 0; // round each of the allocated sizes, keeping track of the deltas for (int i = 0; i < definitions.Count; ++i) @@ -2363,6 +2388,17 @@ namespace Avalonia.Controls grid.SetFlags((bool)e.NewValue!, Flags.ShowGridLinesPropertyValue); } + private static void OnSpacingPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + Grid grid = (Grid)d; + + if (grid._extData != null + && grid.ListenToNotifications) + { + grid.CellsStructureDirty = true; + } + } + private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { if (d is Visual child) @@ -2674,6 +2710,18 @@ namespace Avalonia.Controls public static readonly StyledProperty ShowGridLinesProperty = AvaloniaProperty.Register(nameof(ShowGridLines)); + /// + /// Defines the property. + /// + public static readonly StyledProperty RowSpacingProperty = + AvaloniaProperty.Register(nameof(RowSpacing)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColumnSpacingProperty = + AvaloniaProperty.Register(nameof(ColumnSpacingProperty)); + /// /// Column property. This is an attached property. /// Grid defines Column property, so that it can be set @@ -3269,6 +3317,14 @@ namespace Avalonia.Controls drawingContext, grid.ColumnDefinitions[i].FinalOffset, 0.0, grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height); + + if (grid.ColumnSpacing != 0) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, 0.0, + grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, _lastArrangeSize.Height); + } } for (int i = 1; i < grid.RowDefinitions.Count; ++i) @@ -3277,6 +3333,14 @@ namespace Avalonia.Controls drawingContext, 0.0, grid.RowDefinitions[i].FinalOffset, _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); + + if (grid.RowSpacing != 0) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing, + _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 339a4a7cdd..3eebdde255 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1656,6 +1656,91 @@ namespace Avalonia.Controls.UnitTests Assert.False(grid.IsArrangeValid); } + [Fact] + public void Should_Grid_Controls_With_Spacing() + { + var target = new Grid + { + RowSpacing = 10, + ColumnSpacing = 10, + RowDefinitions = RowDefinitions.Parse("100,100"), + ColumnDefinitions = ColumnDefinitions.Parse("100,100"), + Children = + { + new Border(), + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.RowProperty] = 1 }, + new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 } + } + }; + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 210, 210), target.Bounds); + Assert.Equal(new Rect(0, 0, 100, 100), target.Children[0].Bounds); + Assert.Equal(new Rect(110, 0, 100, 100), target.Children[1].Bounds); + Assert.Equal(new Rect(0, 110, 100, 100), target.Children[2].Bounds); + Assert.Equal(new Rect(110, 110, 100, 100), target.Children[3].Bounds); + } + + [Fact] + public void Should_Grid_Controls_With_Spacing_Complicated() + { + var target = new Grid + { + Width = 200, + Height = 200, + RowSpacing = 10, + ColumnSpacing = 10, + RowDefinitions = RowDefinitions.Parse("50,*,2*,Auto"), + ColumnDefinitions = ColumnDefinitions.Parse("50,*,2*,Auto"), + Children = + { + new Border(), + new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 }, + new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 }, + new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 }, + }, + }; + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 200, 200), target.Bounds); + Assert.Equal(new Rect(0, 0, 50, 50), target.Children[0].Bounds); + Assert.Equal(new Rect(60, 60, 30, 30), target.Children[1].Bounds); + Assert.Equal(new Rect(100, 100, 60, 60), target.Children[2].Bounds); + Assert.Equal(new Rect(170, 170, 30, 30), target.Children[3].Bounds); + } + + [Fact] + public void Should_Grid_Controls_With_Spacing_Overflow() + { + var target = new Grid + { + Width = 100, + Height = 100, + ColumnSpacing = 20, + RowSpacing = 20, + ColumnDefinitions = ColumnDefinitions.Parse("30,*,*,Auto"), + RowDefinitions = RowDefinitions.Parse("30,*,*,Auto"), + Children = + { + new Border(), + new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 }, + new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 }, + new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 }, + }, + }; + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 100, 100), target.Bounds); + Assert.Equal(new Rect(0, 0, 30, 30), target.Children[0].Bounds); + Assert.Equal(new Rect(50, 50, 0, 0), target.Children[1].Bounds); + Assert.Equal(new Rect(70, 70, 0, 0), target.Children[2].Bounds); + Assert.Equal(new Rect(90, 90, 30, 30), target.Children[3].Bounds); + } + private class TestControl : Control { public Size MeasureSize { get; set; }