Browse Source

[Grid] Add RowSpacing and ColumnSpacing (#18077)

* Add spacing properties

* Add spacing while computing final offsets

* Add spacing while calculating desired size

* Add unit tests

* fix missing spacing arrangement

* draw grid line

* Add property changed handler

* add new unit test

* Resolve star spacing

* fix AffectsMeasure

* overflow unit test

* Clean up

---------

Co-authored-by: Poker <poker_sang@outlook.com>
pull/18405/head
Betta_Fish 11 months ago
committed by GitHub
parent
commit
7d3f490b04
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 110
      src/Avalonia.Controls/Grid.cs
  2. 85
      tests/Avalonia.Controls.UnitTests/GridTests.cs

110
src/Avalonia.Controls/Grid.cs

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

85
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -1656,6 +1656,91 @@ namespace Avalonia.Controls.UnitTests
Assert.False(grid.IsArrangeValid); 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 private class TestControl : Control
{ {
public Size MeasureSize { get; set; } public Size MeasureSize { get; set; }

Loading…
Cancel
Save