diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 997e15050f..39c459b307 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -1,6 +1,7 @@  netstandard2.0 + latest false diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 12aa6cf4b8..34c3a9f9fa 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls.Utils { var index = i; var convention = _conventions[index]; - if (convention.Length.IsAuto) + if (convention.Length.IsAuto || convention.Length.IsStar) { foreach (var pair in source.Where(x => x.Value.index <= index && index < x.Value.index + x.Value.span)) @@ -99,10 +99,10 @@ namespace Avalonia.Controls.Utils var aggregatedLength = 0.0; double starUnitLength; - // M2/6. Exclude all the pixel lengths, so that we can calculate the star lengths. + // M2/7. Exclude all the pixel lengths, so that we can calculate the star lengths. aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - // M3/6. Exclude all the * lengths that have reached min value. + // M3/7. Exclude all the * lengths that have reached min value. var shouldTestStarMin = true; while (shouldTestStarMin) { @@ -126,7 +126,7 @@ namespace Avalonia.Controls.Utils shouldTestStarMin = @fixed; } - // M4/6. Exclude all the Auto lengths that have not-zero desired size. + // M4/7. Exclude all the Auto lengths that have not-zero desired size. var shouldTestAuto = true; while (shouldTestAuto) { @@ -140,26 +140,7 @@ namespace Avalonia.Controls.Utils continue; } - var index = i; - var more = 0.0; - foreach (var additional in _additionalConventions) - { - // If the additional conventions contains the Auto column/row, try to determine the Auto column/row length. - if (additional.Index <= index && index < additional.Index + additional.Span) - { - var starUnit = starUnitLength; - var min = Enumerable.Range(additional.Index, additional.Span) - .Select(x => - { - var c = conventions[x]; - if (c.Length.IsAbsolute) return c.Length.Value; - if (c.Length.IsStar) return c.Length.Value * starUnit; - return 0.0; - }).Sum(); - more = Math.Max(additional.Min - min, more); - } - } - + var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); convention.Fix(more); aggregatedLength += more; @fixed = true; @@ -169,12 +150,17 @@ namespace Avalonia.Controls.Utils shouldTestAuto = @fixed; } - // M5/6. Determine the desired length of the grid for current contaienr length. Its value stores in desiredLength. + // M5/7. Expand the stars according to the additional conventions (usually the child desired length). + var desiredStarMin = AggregateAdditionalConventionsForStars(conventions); + aggregatedLength += desiredStarMin; + + + // M6/7. Determine the desired length of the grid for current contaienr length. Its value stores in desiredLength. // But if the container has infinite length, the grid desired length is stored in greedyDesiredLength. var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; var greedyDesiredLength = aggregatedLength; - // M6/6. Expand all the left stars. These stars have no conventions or only have max value so they can be expanded from zero to constrant. + // M7/7. Expand all the left stars. These stars have no conventions or only have max value so they can be expanded from zero to constrant. var dynamicConvention = ExpandStars(conventions, containerLength); Clip(dynamicConvention, containerLength); @@ -202,6 +188,51 @@ namespace Avalonia.Controls.Utils return new ArrangeResult(measure.LengthList); } + [Pure] + private double ApplyAdditionalConventionsForAuto(IReadOnlyList conventions, + int index, double starUnitLength) + { + var more = 0.0; + foreach (var additional in _additionalConventions) + { + // If the additional conventions contains the Auto column/row, try to determine the Auto column/row length. + if (additional.Index <= index && index < additional.Index + additional.Span) + { + var min = Enumerable.Range(additional.Index, additional.Span) + .Select(x => + { + var c = conventions[x]; + if (c.Length.IsAbsolute) return c.Length.Value; + if (c.Length.IsStar) return c.Length.Value * starUnitLength; + return 0.0; + }).Sum(); + more = Math.Max(additional.Min - min, more); + } + } + + return more; + } + + [Pure] + private double AggregateAdditionalConventionsForStars( + IReadOnlyList conventions) + { + // +-----------------------------------------------------------+ + // | * | P | * | P | P | * | P | * | * | + // +-----------------------------------------------------------+ + // |<- x ->| |<- z ->| + // |<- y ->| + // conveniences 是上面看到的那个列表,所有能够确定的 A、P 和 * 都已经转换成了 P;剩下的 * 只有 Max 是没确定的。 + // _additionalConventions 是上面的 x、y、z…… 集合,只有最小值是可用的。 + // 需要返回所有标记为 * 的方格的累加和的最小值。 + + var additionalConventions = _additionalConventions; + + // TODO Calculate the min length of all the desired size. + + return 150; + } + [Pure] private static List ExpandStars(IEnumerable conventions, double constraint) { diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs index 19d1f4e5c3..fbb90de505 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.UnitTests } [Theory] - [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 100d, new[] { 100d, 250d, 250d })] + [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] public void MeasureArrange_ChildHasSize_Correct(string length, IList childLengthList, double containerLength, double expectedDesiredLength, IList expectedLengthList) @@ -136,5 +136,38 @@ namespace Avalonia.Controls.UnitTests var arrange = layout.Arrange(containerLength, measure); Assert.Equal(expectedLengthList, arrange.LengthList); } + + [Theory] + [InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] + [InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] + [InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] + [InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] + [InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] + public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( + double containerLength, double expectedDesiredLength, + IList expectedMeasureLengthList, IList expectedArrangeLengthList) + { + var length = "100,*,2*"; + var childLengthList = new[] { 150d, 150d, 150d }; + var spans = new[] { 1, 2, 1 }; + + // Arrange + var lengthList = new ColumnDefinitions(length); + var layout = new GridLayout(lengthList); + layout.AppendMeasureConventions( + Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), + x => childLengthList[x]); + + // Measure - Action & Assert + var measure = layout.Measure(containerLength); + Assert.Equal(expectedDesiredLength, measure.DesiredLength); + Assert.Equal(expectedMeasureLengthList, measure.LengthList); + + // Arrange - Action & Assert + var arrange = layout.Arrange( + double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, + measure); + Assert.Equal(expectedArrangeLengthList, arrange.LengthList); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/GridMocks.cs b/tests/Avalonia.Controls.UnitTests/GridMocks.cs index 7df604b501..a982882ad8 100644 --- a/tests/Avalonia.Controls.UnitTests/GridMocks.cs +++ b/tests/Avalonia.Controls.UnitTests/GridMocks.cs @@ -6,6 +6,21 @@ namespace Avalonia.Controls.UnitTests { internal static class GridMock { + /// + /// Create a mock grid to test its row layout. + /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). + /// + /// The measure height of this grid. PositiveInfinity by default. + /// The arrange height of this grid. DesiredSize.Height by default. + /// The mock grid that its children bounds will be tested. + internal static Grid New(Size measure = default, Size arrange = default) + { + var grid = new Grid(); + grid.Measure(measure == default ? new Size(double.PositiveInfinity, double.PositiveInfinity) : measure); + grid.Arrange(new Rect(default, arrange == default ? grid.DesiredSize : arrange)); + return grid; + } + /// /// Create a mock grid to test its row layout. /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). @@ -25,7 +40,12 @@ namespace Avalonia.Controls.UnitTests } grid.Measure(new Size(double.PositiveInfinity, measure == default ? double.PositiveInfinity : measure)); - grid.Arrange(new Rect(0, 0, 0, arrange == default ? grid.DesiredSize.Width : arrange)); + if (arrange == default) + { + arrange = measure == default ? grid.DesiredSize.Width : measure; + } + + grid.Arrange(new Rect(0, 0, 0, arrange)); return grid; } @@ -49,7 +69,12 @@ namespace Avalonia.Controls.UnitTests } grid.Measure(new Size(measure == default ? double.PositiveInfinity : measure, double.PositiveInfinity)); - grid.Arrange(new Rect(0, 0, arrange == default ? grid.DesiredSize.Width : arrange, 0)); + if (arrange == default) + { + arrange = measure == default ? grid.DesiredSize.Width : measure; + } + + grid.Arrange(new Rect(0, 0, arrange, 0)); return grid; } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 93c3c9258f..7d649a5a07 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -83,12 +83,23 @@ namespace Avalonia.Controls.UnitTests GridAssert.ChildrenWidth(columnGrid, 50, 100, 150); } + [Fact] + public void Layout_NoRowColumn_BoundsCorrect() + { + // Arrange & Action + var grid = GridMock.New(arrange: new Size(600, 200)); + + // Assert + GridAssert.ChildrenHeight(grid, 600); + GridAssert.ChildrenWidth(grid, 200); + } + [Fact] public void Layout_StarRowColumn_BoundsCorrect() { // Arrange & Action - var rowGrid = GridMock.New(new RowDefinitions("1*,2*,3*"), arrange: 600); - var columnGrid = GridMock.New(new ColumnDefinitions("*,*,2*"), arrange: 600); + var rowGrid = GridMock.New(new RowDefinitions("1*,2*,3*"), 600); + var columnGrid = GridMock.New(new ColumnDefinitions("*,*,2*"), 600); // Assert GridAssert.ChildrenHeight(rowGrid, 100, 200, 300); @@ -99,8 +110,8 @@ namespace Avalonia.Controls.UnitTests public void Layout_MixPixelStarRowColumn_BoundsCorrect() { // Arrange & Action - var rowGrid = GridMock.New(new RowDefinitions("1*,2*,150"), arrange: 600); - var columnGrid = GridMock.New(new ColumnDefinitions("1*,2*,150"), arrange: 600); + var rowGrid = GridMock.New(new RowDefinitions("1*,2*,150"), 600); + var columnGrid = GridMock.New(new ColumnDefinitions("1*,2*,150"), 600); // Assert GridAssert.ChildrenHeight(rowGrid, 150, 300, 150); @@ -116,13 +127,13 @@ namespace Avalonia.Controls.UnitTests new RowDefinition(1, GridUnitType.Star) { MinHeight = 200 }, new RowDefinition(1, GridUnitType.Star), new RowDefinition(1, GridUnitType.Star), - }, arrange: 300); + }, 300); var columnGrid = GridMock.New(new ColumnDefinitions { new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 200 }, new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(1, GridUnitType.Star), - }, arrange: 300); + }, 300); // Assert GridAssert.ChildrenHeight(rowGrid, 200, 50, 50); @@ -138,13 +149,13 @@ namespace Avalonia.Controls.UnitTests new RowDefinition(1, GridUnitType.Star) { MaxHeight = 200 }, new RowDefinition(1, GridUnitType.Star), new RowDefinition(1, GridUnitType.Star), - }, arrange: 800); + }, 800); var columnGrid = GridMock.New(new ColumnDefinitions { new ColumnDefinition(1, GridUnitType.Star) { MaxWidth = 200 }, new ColumnDefinition(1, GridUnitType.Star), new ColumnDefinition(1, GridUnitType.Star), - }, arrange: 800); + }, 800); // Assert GridAssert.ChildrenHeight(rowGrid, 200, 300, 300);