From 38e63d8dc80e9d077c0d7e6a1c08398b4378c519 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 22 Apr 2018 16:35:21 +0800 Subject: [PATCH 01/38] Fix issue#1516 MinHeight/MinWidth deos not work correctly in Grid.ColumnDefinition/RowDefinition. --- src/Avalonia.Controls/Grid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 2a564b6a2c..5c4312f51b 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -726,6 +726,7 @@ namespace Avalonia.Controls double newsize = segmentSize; newsize += contribution * (type == GridUnitType.Star ? matrix[i, i].Stars : 1); newsize = Math.Min(newsize, matrix[i, i].Max); + newsize = Math.Max(newsize, matrix[i, i].Min); assigned |= newsize > segmentSize; size -= newsize - segmentSize; From 5cd5a044ebb94b758dfaf45e2a501e352242be14 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 22 Apr 2018 16:36:00 +0800 Subject: [PATCH 02/38] Add comments for Segment of Grid. --- src/Avalonia.Controls/Grid.cs | 39 ++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 5c4312f51b..63e74d62b1 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -857,15 +857,48 @@ namespace Avalonia.Controls } } + /// + /// Stores the layout values of of of . + /// private struct Segment { + /// + /// Gets or sets the base size of this segment. + /// The value is from the user's code or from the stored measuring values. + /// public double OriginalSize; - public double Max; - public double Min; + + /// + /// Gets the maximum size of this segment. + /// The value is from the user's code. + /// + public readonly double Max; + + /// + /// Gets the minimum size of this segment. + /// The value is from the user's code. + /// + public readonly double Min; + + /// + /// Gets or sets the row/column partial desired size of the . + /// public double DesiredSize; + + /// + /// Gets or sets the row/column offered size that will be used to measure the children. + /// public double OfferedSize; + + /// + /// Gets or sets the star unit size if the is . + /// public double Stars; - public GridUnitType Type; + + /// + /// Gets the segment size unit type. + /// + public readonly GridUnitType Type; public Segment(double offeredSize, double min, double max, GridUnitType type) { From e56f1ab83aa57b3f82992b83d6eca062677ce802 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 22 Apr 2018 17:20:20 +0800 Subject: [PATCH 03/38] If the MinHeight/MinWidth is set and it affects the Grid layout, a new calculation will be triggered. (Not only the MaxHeight/MaxWidth) --- src/Avalonia.Controls/Grid.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 63e74d62b1..02c961c179 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -235,6 +235,9 @@ namespace Avalonia.Controls } else if (height.GridUnitType == GridUnitType.Star) { + _rowMatrix[i, i].OfferedSize = Clamp(0, _rowMatrix[i, i].Min, _rowMatrix[i, i].Max); + _rowMatrix[i, i].DesiredSize = _rowMatrix[i, i].OfferedSize; + _rowMatrix[i, i].Stars = height.Value; totalStarsY += height.Value; } @@ -270,6 +273,9 @@ namespace Avalonia.Controls } else if (width.GridUnitType == GridUnitType.Star) { + _colMatrix[i, i].OfferedSize = Clamp(0, _colMatrix[i, i].Min, _colMatrix[i, i].Max); + _colMatrix[i, i].DesiredSize = _colMatrix[i, i].OfferedSize; + _colMatrix[i, i].Stars = width.Value; totalStarsX += width.Value; } @@ -725,9 +731,10 @@ namespace Avalonia.Controls double newsize = segmentSize; newsize += contribution * (type == GridUnitType.Star ? matrix[i, i].Stars : 1); + double newSizeIgnoringMinMax = newsize; newsize = Math.Min(newsize, matrix[i, i].Max); newsize = Math.Max(newsize, matrix[i, i].Min); - assigned |= newsize > segmentSize; + assigned |= !Equals(newsize, newSizeIgnoringMinMax); size -= newsize - segmentSize; if (desiredSize) From 431407437bca1340780614fca1d7c95aebb6bb29 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 28 Apr 2018 13:40:37 +0800 Subject: [PATCH 04/38] Add unit test for grid layout: 1. Pixel row column 1. Start row column 1. Mix pixel star row column --- .../Avalonia.Controls.UnitTests.csproj | 3 + .../Avalonia.Controls.UnitTests/GridMocks.cs | 98 +++++++++++++++++++ .../Avalonia.Controls.UnitTests/GridTests.cs | 38 +++++++ 3 files changed, 139 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/GridMocks.cs diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index fe7e48e085..32fa6abe29 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -3,6 +3,9 @@ netcoreapp2.0 Library + + latest + diff --git a/tests/Avalonia.Controls.UnitTests/GridMocks.cs b/tests/Avalonia.Controls.UnitTests/GridMocks.cs new file mode 100644 index 0000000000..7df604b501 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/GridMocks.cs @@ -0,0 +1,98 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +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 row definitions of this mock grid. + /// 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. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] + internal static Grid New(RowDefinitions rows, + double measure = default, double arrange = default) + { + var grid = new Grid { RowDefinitions = rows }; + for (var i = 0; i < rows.Count; i++) + { + grid.Children.Add(new Border { [Grid.RowProperty] = i }); + } + + grid.Measure(new Size(double.PositiveInfinity, measure == default ? double.PositiveInfinity : measure)); + grid.Arrange(new Rect(0, 0, 0, arrange == default ? grid.DesiredSize.Width : arrange)); + + return grid; + } + + /// + /// Create a mock grid to test its column layout. + /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). + /// + /// The column definitions of this mock grid. + /// The measure width of this grid. PositiveInfinity by default. + /// The arrange width of this grid. DesiredSize.Width by default. + /// The mock grid that its children bounds will be tested. + [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")] + internal static Grid New(ColumnDefinitions columns, + double measure = default, double arrange = default) + { + var grid = new Grid { ColumnDefinitions = columns }; + for (var i = 0; i < columns.Count; i++) + { + grid.Children.Add(new Border { [Grid.ColumnProperty] = i }); + } + + grid.Measure(new Size(measure == default ? double.PositiveInfinity : measure, double.PositiveInfinity)); + grid.Arrange(new Rect(0, 0, arrange == default ? grid.DesiredSize.Width : arrange, 0)); + + return grid; + } + } + + internal static class GridAssert + { + /// + /// Assert all the children heights. + /// This method will assume that the grid children count equals row count. + /// + /// The children will be fetched through it. + /// Expected row values of every children. + internal static void ChildrenHeight(Grid grid, params double[] rows) + { + if (grid.Children.Count != rows.Length) + { + throw new NotSupportedException(); + } + + for (var i = 0; i < rows.Length; i++) + { + Assert.Equal(rows[i], grid.Children[i].Bounds.Height); + } + } + + /// + /// Assert all the children widths. + /// This method will assume that the grid children count equals row count. + /// + /// The children will be fetched through it. + /// Expected column values of every children. + internal static void ChildrenWidth(Grid grid, params double[] columns) + { + if (grid.Children.Count != columns.Length) + { + throw new NotSupportedException(); + } + + for (var i = 0; i < columns.Length; i++) + { + Assert.Equal(columns[i], grid.Children[i].Bounds.Width); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index c5aea6501f..675408b75d 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Avalonia.Controls; using Xunit; @@ -64,5 +66,41 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 25, 150, 25), target.Children[1].Bounds); Assert.Equal(new Rect(154, 25, 50, 25), target.Children[2].Bounds); } + + [Fact] + public void Layout_PixelRowColumn_BoundsCorrect() + { + // Arrange & Action + var rowGrid = GridMock.New(new RowDefinitions("100,200,300")); + var columnGrid = GridMock.New(new ColumnDefinitions("50,100,150")); + + // Assert + GridAssert.ChildrenHeight(rowGrid, 100, 200, 300); + GridAssert.ChildrenWidth(columnGrid, 50, 100, 150); + } + + [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); + + // Assert + GridAssert.ChildrenHeight(rowGrid, 100, 200, 300); + GridAssert.ChildrenWidth(columnGrid, 150, 150, 300); + } + + [Fact] + 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); + + // Assert + GridAssert.ChildrenHeight(rowGrid, 150, 300, 150); + GridAssert.ChildrenWidth(columnGrid, 150, 300, 150); + } } } From b8bebd634d14bd013b348d6560d1c847a3c290ab Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 28 Apr 2018 13:48:52 +0800 Subject: [PATCH 05/38] Add unit test for grid layout: - start row column with min length. --- .../Avalonia.Controls.UnitTests/GridTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 675408b75d..e739e8edba 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -102,5 +102,27 @@ namespace Avalonia.Controls.UnitTests GridAssert.ChildrenHeight(rowGrid, 150, 300, 150); GridAssert.ChildrenWidth(columnGrid, 150, 300, 150); } + + [Fact] + public void Layout_StarRowColumnWithMinLength_BoundsCorrect() + { + // Arrange & Action + var rowGrid = GridMock.New(new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star) { MinHeight = 200 }, + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(1, GridUnitType.Star), + }, arrange: 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); + + // Assert + GridAssert.ChildrenHeight(rowGrid, 200, 50, 50); + GridAssert.ChildrenWidth(columnGrid, 200, 50, 50); + } } } From b9d71860ff3a2711e27293191960441ddfce9c14 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 28 Apr 2018 21:40:55 +0800 Subject: [PATCH 06/38] Add a new version of Grid layout. and its performance may be better than the original one. --- src/Avalonia.Controls/Utils/GridLayout.cs | 144 ++++++++++++++++++ .../GridLayoutTests.cs | 99 ++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 src/Avalonia.Controls/Utils/GridLayout.cs create mode 100644 tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs new file mode 100644 index 0000000000..2497a5f46c --- /dev/null +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +namespace Avalonia.Controls.Utils +{ + internal class GridLayout + { + internal GridLayout(LengthDefinitions lengths) + { + _lengths = lengths; + } + + private readonly LengthDefinitions _lengths; + + /// + /// Try to calculate the lengths that will be used to measure the children. + /// If the is not enough, we'll even not compress the measure length. + /// + /// The container length, width or height. + /// The lengths that can be used to measure the children. + [Pure] + internal List Measure(double containerLength) + { + var lengths = _lengths.Clone(); + + // Exclude all the pixel lengths, so that we can calculate the star lengths. + containerLength -= lengths + .Where(x => x.Length.IsAbsolute) + .Aggregate(0.0, (sum, add) => sum + add.Length.Value); + + // Aggregate the star count, so that we can determine the length of each star unit. + var starCount = lengths + .Where(x => x.Length.IsStar) + .Aggregate(0.0, (sum, add) => sum + add.Length.Value); + // There is no need to care the (starCount == 0). If this happens, we'll ignore all the stars. + var starUnitLength = containerLength / starCount; + + // If there is no stars, just return all pixels. + if (Equals(starCount, 0.0)) + { + return lengths.Select(x => x.Length.IsAuto ? double.PositiveInfinity : x.Length.Value).ToList(); + } + + // --- + // Warning! The code below will start to change the lengths item value. + // --- + + // Exclude the star unit if its min/max length range does not contain the calculated star length. + var intermediateStarLengths = lengths.Where(x => x.Length.IsStar).ToList(); + // Indicate whether all star lengths are in range of min and max or not. + var allInRange = false; + while (!allInRange) + { + foreach (var length in intermediateStarLengths) + { + // Find out if there is any length out of min to max. + var (star, min, max) = (length.Length.Value, length.MinLength, length.MaxLength); + var starLength = star * starUnitLength; + if (starLength < min || starLength > max) + { + // If the star length is out of min to max, change it to a pixel unit. + if (starLength < min) + { + length.Update(min); + starLength = min; + } + else if (starLength > max) + { + length.Update(max); + starLength = max; + } + + // Update the rest star length info. + intermediateStarLengths.Remove(length); + containerLength -= starLength; + starCount -= star; + starUnitLength = containerLength / starCount; + break; + } + } + + // All lengths are in range, so that we have enough lengths to measure children. + allInRange = true; + foreach (var length in intermediateStarLengths) + { + length.Update(length.Length.Value * starUnitLength); + } + } + + // Return the modified lengths as measuring lengths. + return lengths.Select(x => + x.Length.GridUnitType == GridUnitType.Auto + ? double.PositiveInfinity + : x.Length.Value).ToList(); + } + + internal class LengthDefinitions : IEnumerable, ICloneable + { + private readonly List _lengths; + + private LengthDefinitions(IEnumerable lengths) + { + _lengths = lengths.ToList(); + } + + public IEnumerator GetEnumerator() => _lengths.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + object ICloneable.Clone() => Clone(); + + public LengthDefinitions Clone() => new LengthDefinitions( + _lengths.Select(x => new LengthDefinition(x.Length, x.MinLength, x.MaxLength))); + + public static implicit operator LengthDefinitions(RowDefinitions rows) + => new LengthDefinitions(rows.Select(x => (LengthDefinition) x)); + } + + internal class LengthDefinition + { + internal LengthDefinition(GridLength length, double minLength, double maxLength) + { + Length = length; + MinLength = minLength; + MaxLength = maxLength; + } + + internal GridLength Length { get; private set; } + internal double MinLength { get; } + internal double MaxLength { get; } + + public static implicit operator LengthDefinition(RowDefinition row) + => new LengthDefinition(row.Height, row.MinHeight, row.MaxHeight); + + public void Update(double pixel) + { + Length = new GridLength(pixel); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs new file mode 100644 index 0000000000..be6b6ce465 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -0,0 +1,99 @@ +using Avalonia.Controls.Utils; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class GridLayoutTests + { + [Fact] + public void Measure_AllPixelLength_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("100,200,300")); + + // Action + var measure = layout.Measure(800); + + // Assert + Assert.Equal(measure, new [] { 100d, 200d, 300d }); + } + + [Fact] + public void Measure_AllStarLength_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("*,2*,3*")); + + // Action + var measure = layout.Measure(600); + + // Assert + Assert.Equal(measure, new [] { 100d, 200d, 300d }); + } + + [Fact] + public void Measure_MixStarPixelLength_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("100,2*,3*")); + + // Action + var measure = layout.Measure(600); + + // Assert + Assert.Equal(measure, new [] { 100d, 200d, 300d }); + } + + [Fact] + public void Measure_MixAutoPixelLength_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("100,200,Auto")); + + // Action + var measure = layout.Measure(600); + + // Assert + Assert.Equal(measure, new [] { 100d, 200d, double.PositiveInfinity }); + } + + [Fact] + public void Measure_MixAutoStarLength_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("*,2*,Auto")); + + // Action + var measure = layout.Measure(600); + + // Assert + Assert.Equal(measure, new[] { 200d, 400d, double.PositiveInfinity }); + } + + [Fact] + public void Measure_MixAutoStarPixelLength_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("*,200,Auto")); + + // Action + var measure = layout.Measure(600); + + // Assert + Assert.Equal(measure, new[] { 400d, 200d, double.PositiveInfinity }); + } + + [Fact] + public void Measure_AllPixelLengthButNotEnough_Correct() + { + // Arrange + var layout = new GridLayout(new RowDefinitions("100,200,300")); + + // Action + var measure = layout.Measure(400); + + // Assert + Assert.Equal(measure, new[] { 100d, 200d, 300d }); + } + } +} From c32c6193221ac8a233363c6e94a45b928ec20f46 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sat, 28 Apr 2018 21:48:12 +0800 Subject: [PATCH 07/38] Add column support for grid layout. --- src/Avalonia.Controls/Utils/GridLayout.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 2497a5f46c..439b6830b9 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -117,6 +117,9 @@ namespace Avalonia.Controls.Utils public static implicit operator LengthDefinitions(RowDefinitions rows) => new LengthDefinitions(rows.Select(x => (LengthDefinition) x)); + + public static implicit operator LengthDefinitions(ColumnDefinitions rows) + => new LengthDefinitions(rows.Select(x => (LengthDefinition)x)); } internal class LengthDefinition @@ -135,6 +138,9 @@ namespace Avalonia.Controls.Utils public static implicit operator LengthDefinition(RowDefinition row) => new LengthDefinition(row.Height, row.MinHeight, row.MaxHeight); + public static implicit operator LengthDefinition(ColumnDefinition row) + => new LengthDefinition(row.Width, row.MinWidth, row.MaxWidth); + public void Update(double pixel) { Length = new GridLength(pixel); From b135f988e41c5d5789629fd06dfc8f7f54995896 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 29 Apr 2018 11:25:30 +0800 Subject: [PATCH 08/38] Try to use new algorithm to measure and arrange Grid. --- src/Avalonia.Controls/Grid.cs | 462 +++++------------- src/Avalonia.Controls/Utils/GridLayout.cs | 115 ++++- .../GridLayoutTests.cs | 91 ++++ .../Avalonia.Controls.UnitTests/GridTests.cs | 26 + 4 files changed, 359 insertions(+), 335 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 02c961c179..5991d61fb8 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -4,7 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Collections; +using Avalonia.Controls.Utils; +using JetBrains.Annotations; namespace Avalonia.Controls { @@ -190,299 +193,97 @@ namespace Avalonia.Controls /// The desired size of the control. protected override Size MeasureOverride(Size constraint) { - Size totalSize = constraint; - int colCount = ColumnDefinitions.Count; - int rowCount = RowDefinitions.Count; - double totalStarsX = 0; - double totalStarsY = 0; - bool emptyRows = rowCount == 0; - bool emptyCols = colCount == 0; - bool hasChildren = Children.Count > 0; - - if (emptyRows) - { - rowCount = 1; - } - - if (emptyCols) - { - colCount = 1; - } - - CreateMatrices(rowCount, colCount); - - if (emptyRows) - { - _rowMatrix[0, 0] = new Segment(0, 0, double.PositiveInfinity, GridUnitType.Star); - _rowMatrix[0, 0].Stars = 1.0; - totalStarsY += 1.0; - } - else - { - for (int i = 0; i < rowCount; i++) + // +------- 1. Prepare the children status -------+ + // + + + // +------- ------------------------------ -------+ + + // Normalize the column/columnspan and row/rowspan. + var columnCount = ColumnDefinitions.Count; + var rowCount = RowDefinitions.Count; + var safeColumns = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); + var safeRows = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); + + // +------- 2. -------+ + // + + + // +------- ------------------------------ -------+ + + // Find out the children that should be Measure first (those rows/columns are Auto size.) + var columnLayout = new GridLayout(ColumnDefinitions); + var rowLayout = new GridLayout(RowDefinitions); + var autoSizeColumns = columnLayout.Prepare(); + var autoSizeRows = rowLayout.Prepare(); + + foreach (var pair in safeColumns) + { + var child = pair.Key; + var (column, columnSpan) = pair.Value; + var columnLast = column + columnSpan - 1; + if (autoSizeColumns.Contains(columnLast)) { - RowDefinition rowdef = RowDefinitions[i]; - GridLength height = rowdef.Height; - - rowdef.ActualHeight = double.PositiveInfinity; - _rowMatrix[i, i] = new Segment(0, rowdef.MinHeight, rowdef.MaxHeight, height.GridUnitType); - - if (height.GridUnitType == GridUnitType.Pixel) - { - _rowMatrix[i, i].OfferedSize = Clamp(height.Value, _rowMatrix[i, i].Min, _rowMatrix[i, i].Max); - _rowMatrix[i, i].DesiredSize = _rowMatrix[i, i].OfferedSize; - rowdef.ActualHeight = _rowMatrix[i, i].OfferedSize; - } - else if (height.GridUnitType == GridUnitType.Star) - { - _rowMatrix[i, i].OfferedSize = Clamp(0, _rowMatrix[i, i].Min, _rowMatrix[i, i].Max); - _rowMatrix[i, i].DesiredSize = _rowMatrix[i, i].OfferedSize; - - _rowMatrix[i, i].Stars = height.Value; - totalStarsY += height.Value; - } - else if (height.GridUnitType == GridUnitType.Auto) - { - _rowMatrix[i, i].OfferedSize = Clamp(0, _rowMatrix[i, i].Min, _rowMatrix[i, i].Max); - _rowMatrix[i, i].DesiredSize = _rowMatrix[i, i].OfferedSize; - } + } } - if (emptyCols) + // Calculate row height list and column width list. + var widthList = columnLayout.Measure(constraint.Width); + var heightList = rowLayout.Measure(constraint.Height); + + // Calculate the available width list and height list for every child. + var childrenAvailableWidths = Children.OfType().ToDictionary(child => child, child => { - _colMatrix[0, 0] = new Segment(0, 0, double.PositiveInfinity, GridUnitType.Star); - _colMatrix[0, 0].Stars = 1.0; - totalStarsX += 1.0; - } - else + var (column, columnSpan) = GetSafeSpan(widthList.Count, GetColumn(child), GetColumnSpan(child)); + return Span(widthList, column, columnSpan).Sum(); + }); + var childrenAvailableHeights = Children.OfType().ToDictionary(child => child, child => { - for (int i = 0; i < colCount; i++) - { - ColumnDefinition coldef = ColumnDefinitions[i]; - GridLength width = coldef.Width; - - coldef.ActualWidth = double.PositiveInfinity; - _colMatrix[i, i] = new Segment(0, coldef.MinWidth, coldef.MaxWidth, width.GridUnitType); - - if (width.GridUnitType == GridUnitType.Pixel) - { - _colMatrix[i, i].OfferedSize = Clamp(width.Value, _colMatrix[i, i].Min, _colMatrix[i, i].Max); - _colMatrix[i, i].DesiredSize = _colMatrix[i, i].OfferedSize; - coldef.ActualWidth = _colMatrix[i, i].OfferedSize; - } - else if (width.GridUnitType == GridUnitType.Star) - { - _colMatrix[i, i].OfferedSize = Clamp(0, _colMatrix[i, i].Min, _colMatrix[i, i].Max); - _colMatrix[i, i].DesiredSize = _colMatrix[i, i].OfferedSize; - - _colMatrix[i, i].Stars = width.Value; - totalStarsX += width.Value; - } - else if (width.GridUnitType == GridUnitType.Auto) - { - _colMatrix[i, i].OfferedSize = Clamp(0, _colMatrix[i, i].Min, _colMatrix[i, i].Max); - _colMatrix[i, i].DesiredSize = _colMatrix[i, i].OfferedSize; - } - } - } - - List sizes = new List(); - GridNode node; - GridNode separator = new GridNode(null, 0, 0, 0); - int separatorIndex; - - sizes.Add(separator); + var (row, rowSpan) = GetSafeSpan(heightList.Count, GetRow(child), GetRowSpan(child)); + return Span(heightList, row, rowSpan).Sum(); + }); - // Pre-process the grid children so that we know what types of elements we have so - // we can apply our special measuring rules. - GridWalker gridWalker = new GridWalker(this, _rowMatrix, _colMatrix); + // Measure the children. + var availableWidth = constraint.Width; + var availableHeight = constraint.Height; + var desiredWidth = 0.0; + var desiredHeight = 0.0; + var sortedChildren = Children.OfType() + .OrderBy(GetColumn).ThenBy(GetRow) + .ToDictionary(child => child, child => (GetColumn(child), GetRow(child))); - for (int i = 0; i < 6; i++) + var currentDesiredWidth = 0.0; + var currentDesiredHeight = 0.0; + var currentColumn = 0; + var currentRow = 0; + foreach (var pair in sortedChildren) { - // These bools tell us which grid element type we should be measuring. i.e. - // 'star/auto' means we should measure elements with a star row and auto col - bool autoAuto = i == 0; - bool starAuto = i == 1; - bool autoStar = i == 2; - bool starAutoAgain = i == 3; - bool nonStar = i == 4; - bool remainingStar = i == 5; + var child = pair.Key; + var (column, row) = pair.Value; + child.Measure(new Size(childrenAvailableWidths[child], childrenAvailableHeights[child])); + var desiredSize = child.DesiredSize; - if (hasChildren) + if (column == currentColumn) { - ExpandStarCols(totalSize); - ExpandStarRows(totalSize); + currentDesiredWidth = Math.Max(desiredSize.Width, currentDesiredWidth); } - - foreach (Control child in Children) + else { - int col, row; - int colspan, rowspan; - double childSizeX = 0; - double childSizeY = 0; - bool starCol = false; - bool starRow = false; - bool autoCol = false; - bool autoRow = false; - - col = Math.Min(GetColumn(child), colCount - 1); - row = Math.Min(GetRow(child), rowCount - 1); - colspan = Math.Min(GetColumnSpan(child), colCount - col); - rowspan = Math.Min(GetRowSpan(child), rowCount - row); - - for (int r = row; r < row + rowspan; r++) - { - starRow |= _rowMatrix[r, r].Type == GridUnitType.Star; - autoRow |= _rowMatrix[r, r].Type == GridUnitType.Auto; - } - - for (int c = col; c < col + colspan; c++) - { - starCol |= _colMatrix[c, c].Type == GridUnitType.Star; - autoCol |= _colMatrix[c, c].Type == GridUnitType.Auto; - } - - // This series of if statements checks whether or not we should measure - // the current element and also if we need to override the sizes - // passed to the Measure call. - - // If the element has Auto rows and Auto columns and does not span Star - // rows/cols it should only be measured in the auto_auto phase. - // There are similar rules governing auto/star and star/auto elements. - // NOTE: star/auto elements are measured twice. The first time with - // an override for height, the second time without it. - if (autoRow && autoCol && !starRow && !starCol) - { - if (!autoAuto) - { - continue; - } - - childSizeX = double.PositiveInfinity; - childSizeY = double.PositiveInfinity; - } - else if (starRow && autoCol && !starCol) - { - if (!(starAuto || starAutoAgain)) - { - continue; - } - - if (starAuto && gridWalker.HasAutoStar) - { - childSizeY = double.PositiveInfinity; - } - - childSizeX = double.PositiveInfinity; - } - else if (autoRow && starCol && !starRow) - { - if (!autoStar) - { - continue; - } - - childSizeY = double.PositiveInfinity; - } - else if ((autoRow || autoCol) && !(starRow || starCol)) - { - if (!nonStar) - { - continue; - } - - if (autoRow) - { - childSizeY = double.PositiveInfinity; - } - - if (autoCol) - { - childSizeX = double.PositiveInfinity; - } - } - else if (!(starRow || starCol)) - { - if (!nonStar) - { - continue; - } - } - else - { - if (!remainingStar) - { - continue; - } - } - - for (int r = row; r < row + rowspan; r++) - { - childSizeY += _rowMatrix[r, r].OfferedSize; - } - - for (int c = col; c < col + colspan; c++) - { - childSizeX += _colMatrix[c, c].OfferedSize; - } - - child.Measure(new Size(childSizeX, childSizeY)); - Size desired = child.DesiredSize; - - // Elements distribute their height based on two rules: - // 1) Elements with rowspan/colspan == 1 distribute their height first - // 2) Everything else distributes in a LIFO manner. - // As such, add all UIElements with rowspan/colspan == 1 after the separator in - // the list and everything else before it. Then to process, just keep popping - // elements off the end of the list. - if (!starAuto) - { - node = new GridNode(_rowMatrix, row + rowspan - 1, row, desired.Height); - separatorIndex = sizes.IndexOf(separator); - sizes.Insert(node.Row == node.Column ? separatorIndex + 1 : separatorIndex, node); - } - - node = new GridNode(_colMatrix, col + colspan - 1, col, desired.Width); - - separatorIndex = sizes.IndexOf(separator); - sizes.Insert(node.Row == node.Column ? separatorIndex + 1 : separatorIndex, node); + currentDesiredWidth = desiredSize.Width; + currentColumn = column; + availableWidth -= desiredSize.Width; + desiredHeight -= desiredSize.Width; } - sizes.Remove(separator); - - while (sizes.Count > 0) + if (availableWidth < desiredSize.Width) { - node = sizes.Last(); - node.Matrix[node.Row, node.Column].DesiredSize = Math.Max(node.Matrix[node.Row, node.Column].DesiredSize, node.Size); - AllocateDesiredSize(rowCount, colCount); - sizes.Remove(node); + } - sizes.Add(separator); + availableHeight -= desiredSize.Height; + desiredHeight -= desiredSize.Width; } - // Once we have measured and distributed all sizes, we have to store - // the results. Every time we want to expand the rows/cols, this will - // be used as the baseline. - SaveMeasureResults(); - - sizes.Remove(separator); - - double gridSizeX = 0; - double gridSizeY = 0; - - for (int c = 0; c < colCount; c++) - { - gridSizeX += _colMatrix[c, c].DesiredSize; - } - - for (int r = 0; r < rowCount; r++) - { - gridSizeY += _rowMatrix[r, r].DesiredSize; - } - - return new Size(gridSizeX, gridSizeY); + return constraint; } /// @@ -492,84 +293,77 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { - int colCount = ColumnDefinitions.Count; - int rowCount = RowDefinitions.Count; - int colMatrixDim = _colMatrix.GetLength(0); - int rowMatrixDim = _rowMatrix.GetLength(0); - - RestoreMeasureResults(); + // Calculate row height list and column width list. + var rowLayout = new GridLayout(RowDefinitions); + var columnLayout = new GridLayout(ColumnDefinitions); + var heightList = rowLayout.Measure(finalSize.Height); + var widthList = columnLayout.Measure(finalSize.Width); - double totalConsumedX = 0; - double totalConsumedY = 0; - - for (int c = 0; c < colMatrixDim; c++) + var rowMeasure = new Dictionary(); + var columnMeasure = new Dictionary(); + foreach (var child in Children.OfType().OrderBy(GetRow)) { - _colMatrix[c, c].OfferedSize = _colMatrix[c, c].DesiredSize; - totalConsumedX += _colMatrix[c, c].OfferedSize; + var (row, rowSpan) = GetSafeSpan(heightList.Count, GetRow(child), GetRowSpan(child)); + rowMeasure.Add(child, (row, rowSpan)); } - - for (int r = 0; r < rowMatrixDim; r++) + foreach (var child in Children.OfType().OrderBy(GetColumn)) { - _rowMatrix[r, r].OfferedSize = _rowMatrix[r, r].DesiredSize; - totalConsumedY += _rowMatrix[r, r].OfferedSize; + var (column, columnSpan) = GetSafeSpan(widthList.Count, GetColumn(child), GetColumnSpan(child)); + columnMeasure.Add(child, (column, columnSpan)); } - if (totalConsumedX != finalSize.Width) - { - ExpandStarCols(finalSize); - } + } - if (totalConsumedY != finalSize.Height) + /// + /// Gets the safe row/column and rowspan/columnspan for a specified range. + /// The user may assign the row/column properties out of the row count or column cout, this method helps to keep them in. + /// + /// The rows count or the columns count. + /// The row or column that the user assigned. + /// The rowspan or columnspan that the user assigned. + /// The safe row/column and rowspan/columnspan. + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) + { + var index = userIndex; + var span = userSpan; + if (userIndex > length) { - ExpandStarRows(finalSize); + index = length; + span = 1; } - - for (int c = 0; c < colCount; c++) + else if (userIndex + userSpan > length) { - ColumnDefinitions[c].ActualWidth = _colMatrix[c, c].OfferedSize; + span = length - userIndex + 1; } - for (int r = 0; r < rowCount; r++) + return (index, span); + } + + /// + /// Return part of a list from the specified start index and its span length. + /// If Avalonia upgrade .NET Core to 2.1 and introduce C# 7.2, we can use Span to do this. + /// + [Pure] + private static IEnumerable Span(IList list, int index, int span) + { +#if DEBUG + // We do not verify arguments in RELEASE because this is a private method, + // and we must write the correct code before publishing. + if (index >= list.Count) throw new ArgumentOutOfRangeException(nameof(index)); + if (span <= 1) throw new ArgumentException("Argument span should not be smaller than 1.", nameof(span)); + if (index + span > list.Count) throw new ArgumentOutOfRangeException(nameof(index)); +#endif + if (span == 1) { - RowDefinitions[r].ActualHeight = _rowMatrix[r, r].OfferedSize; + yield return list[index]; + yield break; } - foreach (Control child in Children) + for (var i = index; i < index + span; i++) { - int col = Math.Min(GetColumn(child), colMatrixDim - 1); - int row = Math.Min(GetRow(child), rowMatrixDim - 1); - int colspan = Math.Min(GetColumnSpan(child), colMatrixDim - col); - int rowspan = Math.Min(GetRowSpan(child), rowMatrixDim - row); - - double childFinalX = 0; - double childFinalY = 0; - double childFinalW = 0; - double childFinalH = 0; - - for (int c = 0; c < col; c++) - { - childFinalX += _colMatrix[c, c].OfferedSize; - } - - for (int c = col; c < col + colspan; c++) - { - childFinalW += _colMatrix[c, c].OfferedSize; - } - - for (int r = 0; r < row; r++) - { - childFinalY += _rowMatrix[r, r].OfferedSize; - } - - for (int r = row; r < row + rowspan; r++) - { - childFinalH += _rowMatrix[r, r].OfferedSize; - } - - child.Arrange(new Rect(childFinalX, childFinalY, childFinalW, childFinalH)); + yield return list[i]; } - - return finalSize; } private static double Clamp(double val, double min, double max) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 439b6830b9..55daf2c9ca 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -15,6 +15,115 @@ namespace Avalonia.Controls.Utils private readonly LengthDefinitions _lengths; + /// + /// Find out which rows/columns should be measured first. These rows/columns are those that marked with "Auto" size. + /// These "Auto" size rows/columns behavior like fix-size rows/columns but they can only be determined after Measure. + /// + /// The row/column numbers that should be Measure first. + internal List Prepare() + { + var lengths = _lengths; + return Find().ToList(); + + IEnumerable Find() + { + for (var i = 0; i < lengths.Count; i++) + { + var unitType = lengths[i].Length.GridUnitType; + if (unitType == GridUnitType.Auto) + { + yield return i; + } + } + } + } + + /// + /// Try to calculate the lengths that will be used to measure the children. + /// If the is not enough, we'll even not compress the measure length. + /// So you'd better call first to find out the rows/columns that should be excluded first. + /// + /// + /// The container length (width or height) excluding some rows/columns. + /// Call first to find out the rows/columns that should be excluded. + /// + /// The lengths that can be used to measure the children. + [Pure] + internal List Measure(double containerLength) + { + var lengths = _lengths.Clone(); + + // Exclude all the pixel lengths, so that we can calculate the star lengths. + containerLength -= lengths + .Where(x => x.Length.IsAbsolute) + .Aggregate(0.0, (sum, add) => sum + add.Length.Value); + + // Aggregate the star count, so that we can determine the length of each star unit. + var starCount = lengths + .Where(x => x.Length.IsStar) + .Aggregate(0.0, (sum, add) => sum + add.Length.Value); + // There is no need to care the (starCount == 0). If this happens, we'll ignore all the stars. + var starUnitLength = containerLength / starCount; + + // If there is no stars, just return all pixels. + if (Equals(starCount, 0.0)) + { + return lengths.Select(x => x.Length.IsAuto ? double.PositiveInfinity : x.Length.Value).ToList(); + } + + // --- + // Warning! The code below will start to change the lengths item value. + // --- + + // Exclude the star unit if its min/max length range does not contain the calculated star length. + var intermediateStarLengths = lengths.Where(x => x.Length.IsStar).ToList(); + // Indicate whether all star lengths are in range of min and max or not. + var allInRange = false; + while (!allInRange) + { + foreach (var length in intermediateStarLengths) + { + // Find out if there is any length out of min to max. + var (star, min, max) = (length.Length.Value, length.MinLength, length.MaxLength); + var starLength = star * starUnitLength; + if (starLength < min || starLength > max) + { + // If the star length is out of min to max, change it to a pixel unit. + if (starLength < min) + { + length.Update(min); + starLength = min; + } + else if (starLength > max) + { + length.Update(max); + starLength = max; + } + + // Update the rest star length info. + intermediateStarLengths.Remove(length); + containerLength -= starLength; + starCount -= star; + starUnitLength = containerLength / starCount; + break; + } + } + + // All lengths are in range, so that we have enough lengths to measure children. + allInRange = true; + foreach (var length in intermediateStarLengths) + { + length.Update(length.Length.Value * starUnitLength); + } + } + + // Return the modified lengths as measuring lengths. + return lengths.Select(x => + x.Length.GridUnitType == GridUnitType.Auto + ? double.PositiveInfinity + : x.Length.Value).ToList(); + } + /// /// Try to calculate the lengths that will be used to measure the children. /// If the is not enough, we'll even not compress the measure length. @@ -22,7 +131,7 @@ namespace Avalonia.Controls.Utils /// The container length, width or height. /// The lengths that can be used to measure the children. [Pure] - internal List Measure(double containerLength) + internal List Arrange(double containerLength) { var lengths = _lengths.Clone(); @@ -106,6 +215,10 @@ namespace Avalonia.Controls.Utils _lengths = lengths.ToList(); } + public LengthDefinition this[int index] => _lengths[index]; + + public int Count => _lengths.Count; + public IEnumerator GetEnumerator() => _lengths.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs index be6b6ce465..984ec2f6f6 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -95,5 +95,96 @@ namespace Avalonia.Controls.UnitTests // Assert Assert.Equal(measure, new[] { 100d, 200d, 300d }); } + + //[Fact] + //public void Arrange_AllPixelLength_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("100,200,300")); + + // // Action + // var arrange = layout.Arrange(800); + + // // Assert + // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); + //} + + //[Fact] + //public void Arrange_AllStarLength_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("*,2*,3*")); + + // // Action + // var arrange = layout.Arrange(600); + + // // Assert + // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); + //} + + //[Fact] + //public void Arrange_MixStarPixelLength_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("100,2*,3*")); + + // // Action + // var arrange = layout.Arrange(600); + + // // Assert + // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); + //} + + //[Fact] + //public void Arrange_MixAutoPixelLength_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("100,200,Auto")); + + // // Action + // var arrange = layout.Arrange(600); + + // // Assert + // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); + //} + + //[Fact] + //public void Arrange_MixAutoStarLength_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("*,2*,Auto")); + + // // Action + // var arrange = layout.Arrange(600); + + // // Assert + // Assert.Equal(arrange, new[] { 200d, 400d, double.PositiveInfinity }); + //} + + //[Fact] + //public void Arrange_MixAutoStarPixelLength_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("*,200,Auto")); + + // // Action + // var arrange = layout.Arrange(600); + + // // Assert + // Assert.Equal(arrange, new[] { 400d, 200d, double.PositiveInfinity }); + //} + + //[Fact] + //public void Arrange_AllPixelLengthButNotEnough_Correct() + //{ + // // Arrange + // var layout = new GridLayout(new RowDefinitions("100,200,300")); + + // // Action + // var arrange = layout.Arrange(400); + + // // Assert + // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); + //} } } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index e739e8edba..93c3c9258f 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,8 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq; using Avalonia.Controls; using Xunit; @@ -124,5 +128,27 @@ namespace Avalonia.Controls.UnitTests GridAssert.ChildrenHeight(rowGrid, 200, 50, 50); GridAssert.ChildrenWidth(columnGrid, 200, 50, 50); } + + [Fact] + public void Layout_StarRowColumnWithMaxLength_BoundsCorrect() + { + // Arrange & Action + var rowGrid = GridMock.New(new RowDefinitions + { + new RowDefinition(1, GridUnitType.Star) { MaxHeight = 200 }, + new RowDefinition(1, GridUnitType.Star), + new RowDefinition(1, GridUnitType.Star), + }, arrange: 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); + + // Assert + GridAssert.ChildrenHeight(rowGrid, 200, 300, 300); + GridAssert.ChildrenWidth(columnGrid, 200, 300, 300); + } } } From e403299bb2179437c1d184a9b2377b7ee456da66 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 30 Apr 2018 13:31:23 +0800 Subject: [PATCH 09/38] Use a new algorithm to layout Grid. --- src/Avalonia.Controls/Grid.cs | 175 +++----- src/Avalonia.Controls/Utils/GridLayout.cs | 425 ++++++++++-------- .../GridLayoutTests.cs | 204 +++------ 3 files changed, 353 insertions(+), 451 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 5991d61fb8..a72b5367b6 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -186,6 +186,9 @@ namespace Avalonia.Controls element.SetValue(RowSpanProperty, value); } + private GridLayout.MeasureResult _columnMeasureCache; + private GridLayout.MeasureResult _rowMeasureCache; + /// /// Measures the grid. /// @@ -193,97 +196,45 @@ namespace Avalonia.Controls /// The desired size of the control. protected override Size MeasureOverride(Size constraint) { - // +------- 1. Prepare the children status -------+ - // + + - // +------- ------------------------------ -------+ + var measureCache = new Dictionary(); + var (safeColumns, safeRows) = GetSafeColumnRows(); - // Normalize the column/columnspan and row/rowspan. - var columnCount = ColumnDefinitions.Count; - var rowCount = RowDefinitions.Count; - var safeColumns = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); - var safeRows = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); - - // +------- 2. -------+ - // + + - // +------- ------------------------------ -------+ - - // Find out the children that should be Measure first (those rows/columns are Auto size.) var columnLayout = new GridLayout(ColumnDefinitions); var rowLayout = new GridLayout(RowDefinitions); - var autoSizeColumns = columnLayout.Prepare(); - var autoSizeRows = rowLayout.Prepare(); - - foreach (var pair in safeColumns) - { - var child = pair.Key; - var (column, columnSpan) = pair.Value; - var columnLast = column + columnSpan - 1; - if (autoSizeColumns.Contains(columnLast)) - { - - } - } + columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); + rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); - // Calculate row height list and column width list. - var widthList = columnLayout.Measure(constraint.Width); - var heightList = rowLayout.Measure(constraint.Height); + var columnResult = columnLayout.Measure(constraint.Width); + var rowResult = rowLayout.Measure(constraint.Height); - // Calculate the available width list and height list for every child. - var childrenAvailableWidths = Children.OfType().ToDictionary(child => child, child => - { - var (column, columnSpan) = GetSafeSpan(widthList.Count, GetColumn(child), GetColumnSpan(child)); - return Span(widthList, column, columnSpan).Sum(); - }); - var childrenAvailableHeights = Children.OfType().ToDictionary(child => child, child => + foreach (var child in Children.OfType()) { - var (row, rowSpan) = GetSafeSpan(heightList.Count, GetRow(child), GetRowSpan(child)); - return Span(heightList, row, rowSpan).Sum(); - }); + var (column, columnSpan) = safeColumns[child]; + var (row, rowSpan) = safeRows[child]; + var width = Enumerable.Range(column, columnSpan) + .Select(x => columnResult.LengthList[x].Length.Value).Sum(); + var height = Enumerable.Range(row, rowSpan) + .Select(x => rowResult.LengthList[x].Length.Value).Sum(); - // Measure the children. - var availableWidth = constraint.Width; - var availableHeight = constraint.Height; - var desiredWidth = 0.0; - var desiredHeight = 0.0; - var sortedChildren = Children.OfType() - .OrderBy(GetColumn).ThenBy(GetRow) - .ToDictionary(child => child, child => (GetColumn(child), GetRow(child))); + MeasureOnce(child, new Size(width, height)); + } - var currentDesiredWidth = 0.0; - var currentDesiredHeight = 0.0; - var currentColumn = 0; - var currentRow = 0; - foreach (var pair in sortedChildren) - { - var child = pair.Key; - var (column, row) = pair.Value; - child.Measure(new Size(childrenAvailableWidths[child], childrenAvailableHeights[child])); - var desiredSize = child.DesiredSize; + _columnMeasureCache = columnResult; + _rowMeasureCache = rowResult; + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); - if (column == currentColumn) - { - currentDesiredWidth = Math.Max(desiredSize.Width, currentDesiredWidth); - } - else - { - currentDesiredWidth = desiredSize.Width; - currentColumn = column; - availableWidth -= desiredSize.Width; - desiredHeight -= desiredSize.Width; - } - - if (availableWidth < desiredSize.Width) + Size MeasureOnce(Control child, Size size) + { + if (measureCache.TryGetValue(child, out var desiredSize)) { - + return desiredSize; } - availableHeight -= desiredSize.Height; - desiredHeight -= desiredSize.Width; + child.Measure(size); + desiredSize = child.DesiredSize; + measureCache[child] = desiredSize; + return desiredSize; } - - return constraint; } /// @@ -293,25 +244,39 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { - // Calculate row height list and column width list. - var rowLayout = new GridLayout(RowDefinitions); + var (safeColumns, safeRows) = GetSafeColumnRows(); + var columnLayout = new GridLayout(ColumnDefinitions); - var heightList = rowLayout.Measure(finalSize.Height); - var widthList = columnLayout.Measure(finalSize.Width); + var rowLayout = new GridLayout(RowDefinitions); - var rowMeasure = new Dictionary(); - var columnMeasure = new Dictionary(); - foreach (var child in Children.OfType().OrderBy(GetRow)) - { - var (row, rowSpan) = GetSafeSpan(heightList.Count, GetRow(child), GetRowSpan(child)); - rowMeasure.Add(child, (row, rowSpan)); - } - foreach (var child in Children.OfType().OrderBy(GetColumn)) + var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); + var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); + + foreach (var child in Children.OfType()) { - var (column, columnSpan) = GetSafeSpan(widthList.Count, GetColumn(child), GetColumnSpan(child)); - columnMeasure.Add(child, (column, columnSpan)); + var (column, columnSpan) = safeColumns[child]; + var (row, rowSpan) = safeRows[child]; + var width = Enumerable.Range(column, columnSpan) + .Select(x => columnResult.LengthList[x].Length.Value).Sum(); + var height = Enumerable.Range(row, rowSpan) + .Select(x => rowResult.LengthList[x].Length.Value).Sum(); + + child.Arrange(new Rect(0, 0, width, height)); } + return finalSize; + } + + private (Dictionary safeColumns, + Dictionary safeRows) GetSafeColumnRows() + { + var columnCount = ColumnDefinitions.Count; + var rowCount = RowDefinitions.Count; + var safeColumns = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); + var safeRows = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); + return (safeColumns, safeRows); } /// @@ -340,32 +305,6 @@ namespace Avalonia.Controls return (index, span); } - /// - /// Return part of a list from the specified start index and its span length. - /// If Avalonia upgrade .NET Core to 2.1 and introduce C# 7.2, we can use Span to do this. - /// - [Pure] - private static IEnumerable Span(IList list, int index, int span) - { -#if DEBUG - // We do not verify arguments in RELEASE because this is a private method, - // and we must write the correct code before publishing. - if (index >= list.Count) throw new ArgumentOutOfRangeException(nameof(index)); - if (span <= 1) throw new ArgumentException("Argument span should not be smaller than 1.", nameof(span)); - if (index + span > list.Count) throw new ArgumentOutOfRangeException(nameof(index)); -#endif - if (span == 1) - { - yield return list[index]; - yield break; - } - - for (var i = index; i < index + span; i++) - { - yield return list[i]; - } - } - private static double Clamp(double val, double min, double max) { if (val < min) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 55daf2c9ca..a872c01695 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -1,263 +1,328 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; namespace Avalonia.Controls.Utils { + // We have three kind of unit: + // - * means Star unit. It can be affected by min and max pixel length. + // - A means Auto unit. It can be affected by min/max pixel length and desired pixel length. + // - P means Pixel unit. It is fixed and can't be affected by any other values. + // Notice that some child stands not only one column/row and this affects desired length. + // Desired length behaviors like the min pixel length but: + // - This can only be determined after the Measure. + // + // This is an example indicates how this class stores data. + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // | min | min | | | min | | min max | + // |<- desired ->| + // + // During the measuring procedure: + // - * wants as much as possible space in range of min and max. + // - A wants as less as possible space in range of min/desired and max. + // - P wants a fix-size space. + // But during the arranging procedure: + // - * behaviors the same. + // - A wants as much as possible space in range of min/desired and max. + // - P behaviors the same. + // + /// + /// Contains algorithms that can help to measure and arrange a Grid. + /// internal class GridLayout { - internal GridLayout(LengthDefinitions lengths) + internal GridLayout(ColumnDefinitions columns) { - _lengths = lengths; + _conventions = columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); } - private readonly LengthDefinitions _lengths; + internal GridLayout(RowDefinitions rows) + { + _conventions = rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); + } + + private const double LayoutTolerance = 1.0 / 256.0; + private readonly List _conventions; + private readonly List _additionalConventions = new List(); /// - /// Find out which rows/columns should be measured first. These rows/columns are those that marked with "Auto" size. - /// These "Auto" size rows/columns behavior like fix-size rows/columns but they can only be determined after Measure. + /// Some elements are not in a single grid cell, they have multiple column/row spans, + /// and these elements may affects the grid layout especially the measure procedure. + /// Append these elements into the convention list can help to layout them correctly through their desired size. + /// Only a small subset of grid children need to be measured before layout starts and they are called via the callback. /// - /// The row/column numbers that should be Measure first. - internal List Prepare() + /// + /// + /// + internal void AppendMeasureConventions(IDictionary source, + Func getDesiredLength) { - var lengths = _lengths; - return Find().ToList(); - - IEnumerable Find() + // M1/6. Find all the Auto length columns/rows. + // Only these columns/rows' layout can be affected by the children desired size. + var found = new Dictionary(); + for (var i = 0; i < _conventions.Count; i++) { - for (var i = 0; i < lengths.Count; i++) + var index = i; + var convention = _conventions[index]; + if (convention.Length.IsAuto) { - var unitType = lengths[i].Length.GridUnitType; - if (unitType == GridUnitType.Auto) + foreach (var pair in source.Where(x => + x.Value.index <= index && index < x.Value.index + x.Value.span)) { - yield return i; + found[pair.Key] = pair.Value; } } } - } - /// - /// Try to calculate the lengths that will be used to measure the children. - /// If the is not enough, we'll even not compress the measure length. - /// So you'd better call first to find out the rows/columns that should be excluded first. - /// - /// - /// The container length (width or height) excluding some rows/columns. - /// Call first to find out the rows/columns that should be excluded. - /// - /// The lengths that can be used to measure the children. - [Pure] - internal List Measure(double containerLength) - { - var lengths = _lengths.Clone(); - - // Exclude all the pixel lengths, so that we can calculate the star lengths. - containerLength -= lengths - .Where(x => x.Length.IsAbsolute) - .Aggregate(0.0, (sum, add) => sum + add.Length.Value); - - // Aggregate the star count, so that we can determine the length of each star unit. - var starCount = lengths - .Where(x => x.Length.IsStar) - .Aggregate(0.0, (sum, add) => sum + add.Length.Value); - // There is no need to care the (starCount == 0). If this happens, we'll ignore all the stars. - var starUnitLength = containerLength / starCount; - - // If there is no stars, just return all pixels. - if (Equals(starCount, 0.0)) + // Append these layout into the additional convention list. + foreach (var pair in found) { - return lengths.Select(x => x.Length.IsAuto ? double.PositiveInfinity : x.Length.Value).ToList(); + var t = pair.Key; + var (index, span) = pair.Value; + var desiredLength = getDesiredLength(t); + if (Math.Abs(desiredLength) > LayoutTolerance) + { + _additionalConventions.Add(new AdditionalLengthConvention(index, span, desiredLength)); + } } + } - // --- - // Warning! The code below will start to change the lengths item value. - // --- - - // Exclude the star unit if its min/max length range does not contain the calculated star length. - var intermediateStarLengths = lengths.Where(x => x.Length.IsStar).ToList(); - // Indicate whether all star lengths are in range of min and max or not. - var allInRange = false; - while (!allInRange) + internal MeasureResult Measure(double containerLength) + { + // Initial. + var conventions = _conventions.Select(x => x.Clone()).ToList(); + var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); + var constraint = containerLength; + double starUnitLength; + + // M2/6. Exclude all the pixel lengths, so that we can calculate the star lengths. + constraint -= conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); + + // M3/6. Exclude all the * lengths that have reached min value. + var shouldTestStarMin = true; + while (shouldTestStarMin) { - foreach (var length in intermediateStarLengths) + var @fixed = false; + starUnitLength = constraint / starCount; + foreach (var convention in conventions.Where(x => x.Length.IsStar)) { - // Find out if there is any length out of min to max. - var (star, min, max) = (length.Length.Value, length.MinLength, length.MaxLength); + var (star, min) = (convention.Length.Value, convention.MinLength); var starLength = star * starUnitLength; - if (starLength < min || starLength > max) + if (starLength < min) { - // If the star length is out of min to max, change it to a pixel unit. - if (starLength < min) - { - length.Update(min); - starLength = min; - } - else if (starLength > max) - { - length.Update(max); - starLength = max; - } - - // Update the rest star length info. - intermediateStarLengths.Remove(length); - containerLength -= starLength; + convention.Fix(min); + starLength = min; + constraint -= starLength; starCount -= star; - starUnitLength = containerLength / starCount; + @fixed = true; break; } } - // All lengths are in range, so that we have enough lengths to measure children. - allInRange = true; - foreach (var length in intermediateStarLengths) + shouldTestStarMin = @fixed; + } + + // M4/6. Exclude all the Auto lengths that have not-zero desired size. + var shouldTestAuto = true; + while (shouldTestAuto) + { + var @fixed = false; + starUnitLength = constraint / starCount; + for (var i = 0; i < conventions.Count; i++) { - length.Update(length.Length.Value * starUnitLength); + var convention = conventions[i]; + if (!convention.Length.IsAuto) + { + 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); + } + } + + convention.Fix(more); + constraint -= more; + @fixed = true; + break; } + + shouldTestAuto = @fixed; } - // Return the modified lengths as measuring lengths. - return lengths.Select(x => - x.Length.GridUnitType == GridUnitType.Auto - ? double.PositiveInfinity - : x.Length.Value).ToList(); + // M5/6. 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 = constraint >= 0.0 ? containerLength - constraint : containerLength; + var greedyDesiredLength = containerLength - constraint; + + // 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. + var dynamicConvention = ExpandStars(conventions, containerLength); + + // Stores the measure result. + return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, conventions, dynamicConvention); } - /// - /// Try to calculate the lengths that will be used to measure the children. - /// If the is not enough, we'll even not compress the measure length. - /// - /// The container length, width or height. - /// The lengths that can be used to measure the children. - [Pure] - internal List Arrange(double containerLength) + public ArrangeResult Arrange(double finalLength, MeasureResult measure) { - var lengths = _lengths.Clone(); - - // Exclude all the pixel lengths, so that we can calculate the star lengths. - containerLength -= lengths - .Where(x => x.Length.IsAbsolute) - .Aggregate(0.0, (sum, add) => sum + add.Length.Value); - - // Aggregate the star count, so that we can determine the length of each star unit. - var starCount = lengths - .Where(x => x.Length.IsStar) - .Aggregate(0.0, (sum, add) => sum + add.Length.Value); - // There is no need to care the (starCount == 0). If this happens, we'll ignore all the stars. - var starUnitLength = containerLength / starCount; - - // If there is no stars, just return all pixels. - if (Equals(starCount, 0.0)) + // If the arrange final length does not equal to the measure length, we should measure again. + if (finalLength - measure.ContainerLength > LayoutTolerance) + { + // If the final length is larger, we will rerun the whole measure. + measure = Measure(finalLength); + } + else if (finalLength - measure.ContainerLength < -LayoutTolerance) { - return lengths.Select(x => x.Length.IsAuto ? double.PositiveInfinity : x.Length.Value).ToList(); + // If the final length is smaller, we measure the M6/6 procedure only. + var dynamicConvention = ExpandStars(measure.LeanLengthList, measure.ContainerLength); + measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, + measure.LeanLengthList, dynamicConvention); } - // --- - // Warning! The code below will start to change the lengths item value. - // --- + return new ArrangeResult(measure.LengthList); + } + + [Pure] + private static List ExpandStars(IEnumerable conventions, double constraint) + { + // Initial. + var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); + constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); + var starUnitLength = 0.0; - // Exclude the star unit if its min/max length range does not contain the calculated star length. - var intermediateStarLengths = lengths.Where(x => x.Length.IsStar).ToList(); - // Indicate whether all star lengths are in range of min and max or not. - var allInRange = false; - while (!allInRange) + // M6/6. + if (constraint >= 0) { - foreach (var length in intermediateStarLengths) + var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); + + var shouldTestStarMax = true; + while (shouldTestStarMax) { - // Find out if there is any length out of min to max. - var (star, min, max) = (length.Length.Value, length.MinLength, length.MaxLength); - var starLength = star * starUnitLength; - if (starLength < min || starLength > max) + var @fixed = false; + starUnitLength = constraint / starCount; + foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) { - // If the star length is out of min to max, change it to a pixel unit. - if (starLength < min) - { - length.Update(min); - starLength = min; - } - else if (starLength > max) + var (star, max) = (convention.Length.Value, convention.MaxLength); + var starLength = star * starUnitLength; + if (starLength > max) { - length.Update(max); + convention.Fix(max); starLength = max; + constraint -= starLength; + starCount -= star; + @fixed = true; + break; } - - // Update the rest star length info. - intermediateStarLengths.Remove(length); - containerLength -= starLength; - starCount -= star; - starUnitLength = containerLength / starCount; - break; } - } - // All lengths are in range, so that we have enough lengths to measure children. - allInRange = true; - foreach (var length in intermediateStarLengths) - { - length.Update(length.Length.Value * starUnitLength); + shouldTestStarMax = @fixed; } } - // Return the modified lengths as measuring lengths. - return lengths.Select(x => - x.Length.GridUnitType == GridUnitType.Auto - ? double.PositiveInfinity - : x.Length.Value).ToList(); + foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar)) + { + convention.Fix(starUnitLength * convention.Length.Value); + } + + Debug.Assert(dynamicConvention.All(x => x.Length.IsAbsolute)); + + return dynamicConvention; } - internal class LengthDefinitions : IEnumerable, ICloneable + internal class LengthConvention : ICloneable { - private readonly List _lengths; - - private LengthDefinitions(IEnumerable lengths) + public LengthConvention(GridLength length, double minLength, double maxLength) { - _lengths = lengths.ToList(); + Length = length; + MinLength = minLength; + MaxLength = maxLength; + if (length.IsAbsolute) + { + _isFixed = true; + } } - public LengthDefinition this[int index] => _lengths[index]; + internal GridLength Length { get; private set; } + internal double MinLength { get; } + internal double MaxLength { get; } - public int Count => _lengths.Count; + public void Fix(double pixel) + { + if (_isFixed) + { + throw new InvalidOperationException("Cannot fix the length convention if it is fixed."); + } - public IEnumerator GetEnumerator() => _lengths.GetEnumerator(); + Length = new GridLength(pixel); + _isFixed = true; + } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + private bool _isFixed; object ICloneable.Clone() => Clone(); - public LengthDefinitions Clone() => new LengthDefinitions( - _lengths.Select(x => new LengthDefinition(x.Length, x.MinLength, x.MaxLength))); + internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); + } - public static implicit operator LengthDefinitions(RowDefinitions rows) - => new LengthDefinitions(rows.Select(x => (LengthDefinition) x)); + internal struct AdditionalLengthConvention + { + public int Index { get; } + public int Span { get; } + public double Min { get; } - public static implicit operator LengthDefinitions(ColumnDefinitions rows) - => new LengthDefinitions(rows.Select(x => (LengthDefinition)x)); + public AdditionalLengthConvention(int index, int span, double min) + { + Index = index; + Span = span; + Min = min; + } } - internal class LengthDefinition + internal class MeasureResult { - internal LengthDefinition(GridLength length, double minLength, double maxLength) + internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, + IReadOnlyList leanConventions, IReadOnlyList expandedConventions) { - Length = length; - MinLength = minLength; - MaxLength = maxLength; + ContainerLength = containerLength; + DesiredLength = desiredLength; + GreedyDesiredLength = greedyDesiredLength; + LeanLengthList = leanConventions; + LengthList = expandedConventions.Select(x => x.Length.Value).ToList(); } - internal GridLength Length { get; private set; } - internal double MinLength { get; } - internal double MaxLength { get; } - - public static implicit operator LengthDefinition(RowDefinition row) - => new LengthDefinition(row.Height, row.MinHeight, row.MaxHeight); - - public static implicit operator LengthDefinition(ColumnDefinition row) - => new LengthDefinition(row.Width, row.MinWidth, row.MaxWidth); + public double ContainerLength { get; } + public double DesiredLength { get; } + public double GreedyDesiredLength { get; } + public IReadOnlyList LeanLengthList { get; } + public IReadOnlyList LengthList { get; } + } - public void Update(double pixel) + internal class ArrangeResult + { + public ArrangeResult(IReadOnlyList lengthList) { - Length = new GridLength(pixel); + LengthList = lengthList; } + + public IReadOnlyList LengthList { get; } } } } diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs index 984ec2f6f6..4ffdd24894 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -1,190 +1,88 @@ -using Avalonia.Controls.Utils; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls.Utils; using Xunit; namespace Avalonia.Controls.UnitTests { public class GridLayoutTests { - [Fact] - public void Measure_AllPixelLength_Correct() + [Theory] + [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] + public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) { - // Arrange - var layout = new GridLayout(new RowDefinitions("100,200,300")); - - // Action - var measure = layout.Measure(800); - - // Assert - Assert.Equal(measure, new [] { 100d, 200d, 300d }); + TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } - [Fact] - public void Measure_AllStarLength_Correct() + [Theory] + [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] + public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) { - // Arrange - var layout = new GridLayout(new RowDefinitions("*,2*,3*")); - - // Action - var measure = layout.Measure(600); - - // Assert - Assert.Equal(measure, new [] { 100d, 200d, 300d }); + TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } - [Fact] - public void Measure_MixStarPixelLength_Correct() + [Theory] + [InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] + public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) { - // Arrange - var layout = new GridLayout(new RowDefinitions("100,2*,3*")); - - // Action - var measure = layout.Measure(600); - - // Assert - Assert.Equal(measure, new [] { 100d, 200d, 300d }); + TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } - [Fact] - public void Measure_MixAutoPixelLength_Correct() + [Theory] + [InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] + public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) { - // Arrange - var layout = new GridLayout(new RowDefinitions("100,200,Auto")); - - // Action - var measure = layout.Measure(600); - - // Assert - Assert.Equal(measure, new [] { 100d, 200d, double.PositiveInfinity }); + TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } - [Fact] - public void Measure_MixAutoStarLength_Correct() + [Theory] + [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] + public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) { - // Arrange - var layout = new GridLayout(new RowDefinitions("*,2*,Auto")); - - // Action - var measure = layout.Measure(600); - - // Assert - Assert.Equal(measure, new[] { 200d, 400d, double.PositiveInfinity }); + TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } - [Fact] - public void Measure_MixAutoStarPixelLength_Correct() + [Theory] + [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] + public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) { - // Arrange - var layout = new GridLayout(new RowDefinitions("*,200,Auto")); - - // Action - var measure = layout.Measure(600); - - // Assert - Assert.Equal(measure, new[] { 400d, 200d, double.PositiveInfinity }); + TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } [Fact] - public void Measure_AllPixelLengthButNotEnough_Correct() + public void MeasureArrange_AllPixelLengthButNotEnough_Correct() { // Arrange var layout = new GridLayout(new RowDefinitions("100,200,300")); - // Action + // Measure - Action & Assert var measure = layout.Measure(400); + Assert.Equal(new[] { 100d, 200d, 300d }, measure.LengthList); - // Assert - Assert.Equal(measure, new[] { 100d, 200d, 300d }); + // Arrange - Action & Assert } - //[Fact] - //public void Arrange_AllPixelLength_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("100,200,300")); - - // // Action - // var arrange = layout.Arrange(800); - - // // Assert - // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); - //} - - //[Fact] - //public void Arrange_AllStarLength_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("*,2*,3*")); - - // // Action - // var arrange = layout.Arrange(600); - - // // Assert - // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); - //} - - //[Fact] - //public void Arrange_MixStarPixelLength_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("100,2*,3*")); - - // // Action - // var arrange = layout.Arrange(600); - - // // Assert - // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); - //} - - //[Fact] - //public void Arrange_MixAutoPixelLength_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("100,200,Auto")); - - // // Action - // var arrange = layout.Arrange(600); - - // // Assert - // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); - //} - - //[Fact] - //public void Arrange_MixAutoStarLength_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("*,2*,Auto")); - - // // Action - // var arrange = layout.Arrange(600); - - // // Assert - // Assert.Equal(arrange, new[] { 200d, 400d, double.PositiveInfinity }); - //} - - //[Fact] - //public void Arrange_MixAutoStarPixelLength_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("*,200,Auto")); - - // // Action - // var arrange = layout.Arrange(600); - - // // Assert - // Assert.Equal(arrange, new[] { 400d, 200d, double.PositiveInfinity }); - //} - - //[Fact] - //public void Arrange_AllPixelLengthButNotEnough_Correct() - //{ - // // Arrange - // var layout = new GridLayout(new RowDefinitions("100,200,300")); + [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] + private static void TestRowDefinitionsOnly(string length, double containerLength, + double expectedDesiredLength, IList expectedLengthList) + { + // Arrange + var layout = new GridLayout(new RowDefinitions(length)); - // // Action - // var arrange = layout.Arrange(400); + // Measure - Action & Assert + var measure = layout.Measure(containerLength); + Assert.Equal(expectedDesiredLength, measure.DesiredLength); + Assert.Equal(expectedLengthList, measure.LengthList); - // // Assert - // Assert.Equal(arrange, new[] { 100d, 200d, 300d }); - //} + // Arrange - Action & Assert + var arrange = layout.Arrange(containerLength, measure); + Assert.Equal(expectedLengthList, arrange.LengthList); + } } } From fc73d7cc374822e7cd3337a9a88ec8461022c157 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 30 Apr 2018 15:35:57 +0800 Subject: [PATCH 10/38] When the available length is not enough, clip the measure list. --- src/Avalonia.Controls/Grid.cs | 12 +++---- src/Avalonia.Controls/Utils/GridLayout.cs | 32 +++++++++++++++---- .../GridLayoutTests.cs | 15 ++------- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index a72b5367b6..ed484ec378 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -211,10 +211,8 @@ namespace Avalonia.Controls { var (column, columnSpan) = safeColumns[child]; var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan) - .Select(x => columnResult.LengthList[x].Length.Value).Sum(); - var height = Enumerable.Range(row, rowSpan) - .Select(x => rowResult.LengthList[x].Length.Value).Sum(); + var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); + var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); MeasureOnce(child, new Size(width, height)); } @@ -256,10 +254,8 @@ namespace Avalonia.Controls { var (column, columnSpan) = safeColumns[child]; var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan) - .Select(x => columnResult.LengthList[x].Length.Value).Sum(); - var height = Enumerable.Range(row, rowSpan) - .Select(x => rowResult.LengthList[x].Length.Value).Sum(); + var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); + var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); child.Arrange(new Rect(0, 0, width, height)); } diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index a872c01695..e77d8d8d88 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -176,9 +176,11 @@ namespace Avalonia.Controls.Utils // 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. var dynamicConvention = ExpandStars(conventions, containerLength); + Clip(dynamicConvention, containerLength); - // Stores the measure result. - return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, conventions, dynamicConvention); + // Stores the measuring result. + return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, + conventions, dynamicConvention); } public ArrangeResult Arrange(double finalLength, MeasureResult measure) @@ -201,7 +203,7 @@ namespace Avalonia.Controls.Utils } [Pure] - private static List ExpandStars(IEnumerable conventions, double constraint) + private static List ExpandStars(IEnumerable conventions, double constraint) { // Initial. var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); @@ -244,7 +246,25 @@ namespace Avalonia.Controls.Utils Debug.Assert(dynamicConvention.All(x => x.Length.IsAbsolute)); - return dynamicConvention; + return dynamicConvention.Select(x => x.Length.Value).ToList(); + } + + private static void Clip(IList lengthList, double constraint) + { + var measureLength = 0.0; + for (var i = 0; i < lengthList.Count; i++) + { + var length = lengthList[i]; + if (constraint - measureLength > length) + { + measureLength += length; + } + else + { + lengthList[i] = constraint - measureLength; + measureLength = constraint; + } + } } internal class LengthConvention : ICloneable @@ -299,13 +319,13 @@ namespace Avalonia.Controls.Utils internal class MeasureResult { internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, - IReadOnlyList leanConventions, IReadOnlyList expandedConventions) + IReadOnlyList leanConventions, IReadOnlyList expandedConventions) { ContainerLength = containerLength; DesiredLength = desiredLength; GreedyDesiredLength = greedyDesiredLength; LeanLengthList = leanConventions; - LengthList = expandedConventions.Select(x => x.Length.Value).ToList(); + LengthList = expandedConventions; } public double ContainerLength { get; } diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs index 4ffdd24894..c28b5f044d 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -9,6 +9,8 @@ namespace Avalonia.Controls.UnitTests { [Theory] [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] + [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] + [InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) { @@ -55,19 +57,6 @@ namespace Avalonia.Controls.UnitTests TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); } - [Fact] - public void MeasureArrange_AllPixelLengthButNotEnough_Correct() - { - // Arrange - var layout = new GridLayout(new RowDefinitions("100,200,300")); - - // Measure - Action & Assert - var measure = layout.Measure(400); - Assert.Equal(new[] { 100d, 200d, 300d }, measure.LengthList); - - // Arrange - Action & Assert - } - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] private static void TestRowDefinitionsOnly(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) From a0518955f50459664650489963cdea5e8bd0ca23 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 30 Apr 2018 15:47:25 +0800 Subject: [PATCH 11/38] Grid layout support positiveinfinity measure. --- src/Avalonia.Controls/Utils/GridLayout.cs | 24 ++++++++++++------- .../GridLayoutTests.cs | 4 ++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index e77d8d8d88..717c696296 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -96,18 +96,18 @@ namespace Avalonia.Controls.Utils // Initial. var conventions = _conventions.Select(x => x.Clone()).ToList(); var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - var constraint = containerLength; + var aggregatedLength = 0.0; double starUnitLength; // M2/6. Exclude all the pixel lengths, so that we can calculate the star lengths. - constraint -= conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); + aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); // M3/6. Exclude all the * lengths that have reached min value. var shouldTestStarMin = true; while (shouldTestStarMin) { var @fixed = false; - starUnitLength = constraint / starCount; + starUnitLength = (containerLength - aggregatedLength) / starCount; foreach (var convention in conventions.Where(x => x.Length.IsStar)) { var (star, min) = (convention.Length.Value, convention.MinLength); @@ -116,7 +116,7 @@ namespace Avalonia.Controls.Utils { convention.Fix(min); starLength = min; - constraint -= starLength; + aggregatedLength += starLength; starCount -= star; @fixed = true; break; @@ -131,7 +131,7 @@ namespace Avalonia.Controls.Utils while (shouldTestAuto) { var @fixed = false; - starUnitLength = constraint / starCount; + starUnitLength = (containerLength - aggregatedLength) / starCount; for (var i = 0; i < conventions.Count; i++) { var convention = conventions[i]; @@ -161,7 +161,7 @@ namespace Avalonia.Controls.Utils } convention.Fix(more); - constraint -= more; + aggregatedLength += more; @fixed = true; break; } @@ -171,8 +171,8 @@ namespace Avalonia.Controls.Utils // M5/6. 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 = constraint >= 0.0 ? containerLength - constraint : containerLength; - var greedyDesiredLength = containerLength - constraint; + 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. var dynamicConvention = ExpandStars(conventions, containerLength); @@ -220,7 +220,8 @@ namespace Avalonia.Controls.Utils { var @fixed = false; starUnitLength = constraint / starCount; - foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) + foreach (var convention in dynamicConvention.Where(x => + x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) { var (star, max) = (convention.Length.Value, convention.MaxLength); var starLength = star * starUnitLength; @@ -239,6 +240,11 @@ namespace Avalonia.Controls.Utils } } + if (double.IsInfinity(starUnitLength)) + { + starUnitLength = 0.0; + } + foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar)) { convention.Fix(starUnitLength * convention.Length.Value); diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs index c28b5f044d..981adc2682 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -8,6 +8,8 @@ namespace Avalonia.Controls.UnitTests public class GridLayoutTests { [Theory] + [InlineData("100, 200, 300", double.PositiveInfinity, 600d, new[] { 100d, 200d, 300d })] + [InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] [InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] @@ -18,6 +20,8 @@ namespace Avalonia.Controls.UnitTests } [Theory] + [InlineData("*,2*,3*", double.PositiveInfinity, 0d, new[] { 0d, 0d, 0d })] + [InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) From cae8a2ab5cd1745ecf5cc73100690a50ed4e4dd7 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 30 Apr 2018 15:53:03 +0800 Subject: [PATCH 12/38] Add not enough length unit test for all test facts. --- src/Avalonia.Controls/Utils/GridLayout.cs | 25 +++++--- .../GridLayoutTests.cs | 63 ++++++++++++++++++- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 717c696296..12aa6cf4b8 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -194,7 +194,7 @@ namespace Avalonia.Controls.Utils else if (finalLength - measure.ContainerLength < -LayoutTolerance) { // If the final length is smaller, we measure the M6/6 procedure only. - var dynamicConvention = ExpandStars(measure.LeanLengthList, measure.ContainerLength); + var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, measure.LeanLengthList, dynamicConvention); } @@ -240,23 +240,28 @@ namespace Avalonia.Controls.Utils } } - if (double.IsInfinity(starUnitLength)) - { - starUnitLength = 0.0; - } + Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto)); - foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar)) + var starUnit = starUnitLength; + var result = dynamicConvention.Select(x => { - convention.Fix(starUnitLength * convention.Length.Value); - } + if (x.Length.IsStar) + { + return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value; + } - Debug.Assert(dynamicConvention.All(x => x.Length.IsAbsolute)); + return x.Length.Value; + }).ToList(); - return dynamicConvention.Select(x => x.Length.Value).ToList(); + return result; } private static void Clip(IList lengthList, double constraint) { + if (double.IsInfinity(constraint)) + { + return; + } var measureLength = 0.0; for (var i = 0; i < lengthList.Count; i++) { diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs index 981adc2682..19d1f4e5c3 100644 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Avalonia.Controls.Utils; using Xunit; @@ -7,8 +8,9 @@ namespace Avalonia.Controls.UnitTests { public class GridLayoutTests { + private const double Inf = double.PositiveInfinity; + [Theory] - [InlineData("100, 200, 300", double.PositiveInfinity, 600d, new[] { 100d, 200d, 300d })] [InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] @@ -20,7 +22,6 @@ namespace Avalonia.Controls.UnitTests } [Theory] - [InlineData("*,2*,3*", double.PositiveInfinity, 0d, new[] { 0d, 0d, 0d })] [InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, @@ -30,7 +31,10 @@ namespace Avalonia.Controls.UnitTests } [Theory] + [InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] + [InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] + [InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) { @@ -38,7 +42,12 @@ namespace Avalonia.Controls.UnitTests } [Theory] + [InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] + [InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] + [InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] + [InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] + [InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) { @@ -46,6 +55,7 @@ namespace Avalonia.Controls.UnitTests } [Theory] + [InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) @@ -54,7 +64,10 @@ namespace Avalonia.Controls.UnitTests } [Theory] + [InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] + [InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] + [InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, double expectedDesiredLength, IList expectedLengthList) { @@ -77,5 +90,51 @@ namespace Avalonia.Controls.UnitTests var arrange = layout.Arrange(containerLength, measure); Assert.Equal(expectedLengthList, arrange.LengthList); } + + [Theory] + [InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] + [InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] + [InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] + [InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] + [InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] + [InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] + public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, + IList expectedMeasureList, IList expectedArrangeList) + { + // Arrange + var layout = new GridLayout(new RowDefinitions(length)); + + // Measure - Action & Assert + var measure = layout.Measure(Inf); + Assert.Equal(expectedDesiredLength, measure.DesiredLength); + Assert.Equal(expectedMeasureList, measure.LengthList); + + // Arrange - Action & Assert + var arrange = layout.Arrange(measure.DesiredLength, measure); + Assert.Equal(expectedArrangeList, arrange.LengthList); + } + + [Theory] + [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 100d, new[] { 100d, 250d, 250d })] + public void MeasureArrange_ChildHasSize_Correct(string length, + IList childLengthList, double containerLength, + double expectedDesiredLength, IList expectedLengthList) + { + // Arrange + var lengthList = new ColumnDefinitions(length); + var layout = new GridLayout(lengthList); + layout.AppendMeasureConventions( + Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), + x => childLengthList[x]); + + // Measure - Action & Assert + var measure = layout.Measure(containerLength); + Assert.Equal(expectedDesiredLength, measure.DesiredLength); + Assert.Equal(expectedLengthList, measure.LengthList); + + // Arrange - Action & Assert + var arrange = layout.Arrange(containerLength, measure); + Assert.Equal(expectedLengthList, arrange.LengthList); + } } } From 163744b89b96ebb529591183b7f044dffb541bca Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 30 Apr 2018 19:01:52 +0800 Subject: [PATCH 13/38] Add support for layout that the children have multi span but also have desired size. --- .../Avalonia.Controls.csproj | 1 + src/Avalonia.Controls/Utils/GridLayout.cs | 83 +++++++++++++------ .../GridLayoutTests.cs | 35 +++++++- .../Avalonia.Controls.UnitTests/GridMocks.cs | 29 ++++++- .../Avalonia.Controls.UnitTests/GridTests.cs | 27 ++++-- 5 files changed, 138 insertions(+), 37 deletions(-) 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); From 63af32bc65c0d7bd8eb82b9be07e591ba5e526d7 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 09:46:35 +0800 Subject: [PATCH 14/38] Add some document comments for GridLayout. --- src/Avalonia.Controls/Utils/GridLayout.cs | 210 +++++++++++++++++----- 1 file changed, 163 insertions(+), 47 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 34c3a9f9fa..f2a5d57725 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -2,67 +2,100 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Avalonia.Layout; using JetBrains.Annotations; namespace Avalonia.Controls.Utils { - // We have three kind of unit: - // - * means Star unit. It can be affected by min and max pixel length. - // - A means Auto unit. It can be affected by min/max pixel length and desired pixel length. - // - P means Pixel unit. It is fixed and can't be affected by any other values. - // Notice that some child stands not only one column/row and this affects desired length. - // Desired length behaviors like the min pixel length but: - // - This can only be determined after the Measure. - // - // This is an example indicates how this class stores data. - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | min | | | min | | min max | - // |<- desired ->| - // - // During the measuring procedure: - // - * wants as much as possible space in range of min and max. - // - A wants as less as possible space in range of min/desired and max. - // - P wants a fix-size space. - // But during the arranging procedure: - // - * behaviors the same. - // - A wants as much as possible space in range of min/desired and max. - // - P behaviors the same. - // /// /// Contains algorithms that can help to measure and arrange a Grid. /// internal class GridLayout { - internal GridLayout(ColumnDefinitions columns) + /// + /// Initialize a new instance from the column definitions. + /// will forget that the layout data comes from the columns. + /// + internal GridLayout([NotNull] ColumnDefinitions columns) { + if (columns == null) throw new ArgumentNullException(nameof(columns)); _conventions = columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); } - internal GridLayout(RowDefinitions rows) + /// + /// Initialize a new instance from the row definitions. + /// will forget that the layout data comes from the rows. + /// + internal GridLayout([NotNull] RowDefinitions rows) { + if (rows == null) throw new ArgumentNullException(nameof(rows)); _conventions = rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); } + /// + /// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same. + /// private const double LayoutTolerance = 1.0 / 256.0; + + /// + /// Gets all the length conventions that come from column/row definitions. + /// These conventions provide limitations of each grid cell. + /// + [NotNull] private readonly List _conventions; + + /// + /// Gets all the length conventions that come from the grid children. + /// + [NotNull] private readonly List _additionalConventions = new List(); /// - /// Some elements are not in a single grid cell, they have multiple column/row spans, - /// and these elements may affects the grid layout especially the measure procedure. - /// Append these elements into the convention list can help to layout them correctly through their desired size. - /// Only a small subset of grid children need to be measured before layout starts and they are called via the callback. + /// Some elements are not only in a single grid cell, they have one or more column/row spans, + /// and these elements may affect the grid layout especially the measuring procedure. + /// Append these elements into the convention list can help to layout them correctly through + /// their desired size. Only a small subset of grid children need to be measured before layout + /// starts and they will be called via the callback. /// - /// - /// - /// - internal void AppendMeasureConventions(IDictionary source, - Func getDesiredLength) + /// The grid children type. + /// + /// Contains the safe column/row index and its span. + /// Notice that we will not verify whether the range is in the column/row count, + /// so you should get the safe column/row info first. + /// + /// + /// This callback will be called if the thinks that a child should be + /// measured first. Usually, these are the children that have the * or Auto length. + /// + internal void AppendMeasureConventions([NotNull] IDictionary source, + [NotNull] Func getDesiredLength) { - // M1/6. Find all the Auto length columns/rows. + if (source == null) throw new ArgumentNullException(nameof(source)); + if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); + + // M1/7. Find all the Auto and * length columns/rows. // Only these columns/rows' layout can be affected by the children desired size. + // + // Find all columns/rows that has the length Auto or *. We'll measure the children in advance. + // Only these kind of columns/rows will affects the Grid layout. + // Please note: + // - The columns/rows of Auto length will definitely be affected by the children size; + // - However, only the Grid.DesiredSize can be affected by the *-length columns/rows unless the Grid has very much space (Infinitely). + + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // _conventions: | min | max | | | min | | min max | max | + // _additionalC: |<- desired ->| |< desired >| + // _additionalC: |< desired >| |<- desired ->| + + // 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。 + // 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。 + // 请注意: + // - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度; + // - 而对于 * 长度,一般只有 Grid.DesiredSize 会受到子元素布局影响,只有在 Grid 布局空间充足时行列长度才会被子元素布局影响。 + + // Find all the Auto and * length columns/rows. var found = new Dictionary(); for (var i = 0; i < _conventions.Count; i++) { @@ -91,21 +124,57 @@ namespace Avalonia.Controls.Utils } } + /// + /// Run measure procedure according to the and gets the . + /// + /// + /// The container length. Usually, it is the constraint of the method. + /// + /// + /// The measured result that containing the desired size and all the column/row length. + /// + [NotNull] internal MeasureResult Measure(double containerLength) { - // Initial. + // Prepare all the variables that this method needs to use. var conventions = _conventions.Select(x => x.Clone()).ToList(); var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); var aggregatedLength = 0.0; double starUnitLength; - // M2/7. Exclude all the pixel lengths, so that we can calculate the star lengths. + // M2/7. Aggregate all the pixel lengths. Then we can get the rest length by `containerLength - aggregatedLength`. + // We mark the aggregated length as "fix" because we can completely determine their values. Same as below. + // + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // |#fix#| |#fix#| + // + // 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。 + // 我们会将所有能够确定下长度的行列标记为 fix。下同。 + // 请注意: + // - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。 + aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - // M3/7. Exclude all the * lengths that have reached min value. + // M3/7. Fix all the * lengths that have reached the minimum. + // + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // | min | max | | | min | | min max | max | + // | fix | |#fix#| fix | + var shouldTestStarMin = true; while (shouldTestStarMin) { + // Calculate the unit * length to estimate the length of each column/row that has * length. + // Under this estimated length, look for if there is a minimum value that has a length less than its constraint. + // If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value. + // + // 计算单位 * 的长度,以便预估出每一个 * 行列的长度。 + // 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。 + // 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。 var @fixed = false; starUnitLength = (containerLength - aggregatedLength) / starCount; foreach (var convention in conventions.Where(x => x.Length.IsStar)) @@ -126,7 +195,14 @@ namespace Avalonia.Controls.Utils shouldTestStarMin = @fixed; } - // M4/7. Exclude all the Auto lengths that have not-zero desired size. + // M3/7. Fix all the Auto lengths that the children on its column/row have a zero or non-zero length. + // + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // | min | max | | | min | | min max | max | + // |#fix#| | fix |#fix#| fix | fix | + var shouldTestAuto = true; while (shouldTestAuto) { @@ -151,26 +227,66 @@ namespace Avalonia.Controls.Utils } // M5/7. Expand the stars according to the additional conventions (usually the child desired length). + // We can't fix this kind of length, so we just mark them as desired (des). + // + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // | min | max | | | min | | min max | max | + // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| + 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. + // M6/7. Determine the desired length of the grid for current container length. Its value stores in desiredLength. + // Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength. + // + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // | min | max | | | min | | min max | max | + // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| + // + // desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des) + // greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des + var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; var greedyDesiredLength = aggregatedLength; - // 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. + // M7/7. Expand all the rest stars. These stars have no conventions or only have + // max value they can be expanded from zero to constraint. + // + // +-----------------------------------------------------------+ + // | * | A | * | P | A | * | P | * | * | + // +-----------------------------------------------------------+ + // | min | max | | | min | | min max | max | + // |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#| + var dynamicConvention = ExpandStars(conventions, containerLength); Clip(dynamicConvention, containerLength); - // Stores the measuring result. + // Returns the measuring result. return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, conventions, dynamicConvention); } - public ArrangeResult Arrange(double finalLength, MeasureResult measure) + /// + /// Run arrange procedure according to the and gets the . + /// + /// + /// The container length. Usually, it is the finalSize of the method. + /// + /// + /// The result that the measuring procedure returns. If it is null, a new measure procedure will run. + /// + /// + /// The measured result that containing the desired size and all the column/row length. + /// + [NotNull] + public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) { + measure = measure ?? Measure(finalLength); + // If the arrange final length does not equal to the measure length, we should measure again. if (finalLength - measure.ContainerLength > LayoutTolerance) { @@ -210,7 +326,7 @@ namespace Avalonia.Controls.Utils } } - return more; + return Math.Min(conventions[index].MaxLength, more); } [Pure] From 0110f7af65553a266b4cb78e0826677c543bb9d4 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 10:04:37 +0800 Subject: [PATCH 15/38] Add some comments for GridLayout helper classes. --- src/Avalonia.Controls/Utils/GridLayout.cs | 141 +++++++++++++++++++++- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index f2a5d57725..93d83fe29a 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -246,6 +246,7 @@ namespace Avalonia.Controls.Utils // +-----------------------------------------------------------+ // | min | max | | | min | | min max | max | // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| + // Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure. // // desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des) // greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des @@ -261,6 +262,7 @@ namespace Avalonia.Controls.Utils // +-----------------------------------------------------------+ // | min | max | | | min | | min max | max | // |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#| + // Note: This table will be stored as the final result into the MeasureResult. var dynamicConvention = ExpandStars(conventions, containerLength); Clip(dynamicConvention, containerLength); @@ -304,10 +306,27 @@ namespace Avalonia.Controls.Utils return new ArrangeResult(measure.LengthList); } + /// + /// + /// + /// + /// + /// + /// [Pure] private double ApplyAdditionalConventionsForAuto(IReadOnlyList conventions, int index, double starUnitLength) { + // 1. Calculate all the * length with starUnitLength. + // 2. Exclude all the fixed length and all the * length. + // 3. The rest of the desired length. + // +-----------------+ + // | * | A | * | + // +-----------------+ + // | exl | | exl | + // |< desired >| + // |< desired >| + var more = 0.0; foreach (var additional in _additionalConventions) { @@ -349,6 +368,13 @@ namespace Avalonia.Controls.Utils return 150; } + /// + /// This method implement the last procedure (M7/7) of measure. + /// It expand all the * length to the fixed length according to the . + /// + /// All the conventions that have almost been fixed except the rest *. + /// The container length. + /// The final pixel length list. [Pure] private static List ExpandStars(IEnumerable conventions, double constraint) { @@ -403,7 +429,14 @@ namespace Avalonia.Controls.Utils return result; } - private static void Clip(IList lengthList, double constraint) + /// + /// If the container length is not infinity. It may be not enough to contain all the columns/rows. + /// We should clip the columns/rows that have been out of the container bounds. + /// Note: This method may change the items value of . + /// + /// All the column width list or the row height list with fixed pixel length. + /// the container length. It can be positive infinity. + private static void Clip([NotNull] IList lengthList, double constraint) { if (double.IsInfinity(constraint)) { @@ -425,8 +458,16 @@ namespace Avalonia.Controls.Utils } } + /// + /// Contains the convention of each column/row. + /// This is mostly the same as or . + /// We use this because we can treat the column and the row the same. + /// internal class LengthConvention : ICloneable { + /// + /// Initialize a new instance of . + /// public LengthConvention(GridLength length, double minLength, double maxLength) { Length = length; @@ -438,10 +479,31 @@ namespace Avalonia.Controls.Utils } } + /// + /// Gets the of a column or a row. + /// internal GridLength Length { get; private set; } + + /// + /// Gets the minimum convention for a column or a row. + /// internal double MinLength { get; } + + /// + /// Gets the maximum convention for a column or a row. + /// internal double MaxLength { get; } + /// + /// Fix the . If all columns/rows are fixed, + /// we can get the double pixel list of all columns/row. + /// + /// + /// The pixel length that should be used to fix the convention. + /// + /// + /// If the convention is pixel length, this exception will throw. + /// public void Fix(double pixel) { if (_isFixed) @@ -453,29 +515,64 @@ namespace Avalonia.Controls.Utils _isFixed = true; } + /// + /// Gets a value that indicates whether this convention is fixed. + /// private bool _isFixed; + /// object ICloneable.Clone() => Clone(); + /// + /// Get a deep copy of this convention list. + /// We need this because we want to store some intermediate states. + /// internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); } + /// + /// Contains the convention that comes from the grid children. + /// Some child span multiple columns or rows, so even a simple column/row can have multiple conventions. + /// internal struct AdditionalLengthConvention { - public int Index { get; } - public int Span { get; } - public double Min { get; } - + /// + /// Initialize a new instance of . + /// public AdditionalLengthConvention(int index, int span, double min) { Index = index; Span = span; Min = min; } + + /// + /// Gets the start index of this additional convention. + /// + public int Index { get; } + + /// + /// Gets the span of this additional convention. + /// + public int Span { get; } + + /// + /// Gets the minimum length of this additional convention. + /// This value is usually provided by the child's desired length. + /// + public double Min { get; } } + /// + /// Stores the result of the measuring procedure. + /// This result can be used to measure children and assign the desired size. + /// Passing this result to can reduce calculation. + /// internal class MeasureResult { + /// + /// Initialize a new instance of . + /// internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, IReadOnlyList leanConventions, IReadOnlyList expandedConventions) { @@ -486,20 +583,52 @@ namespace Avalonia.Controls.Utils LengthList = expandedConventions; } + /// + /// Gets the container length for this result. + /// This property exists because a measure result is related to it. + /// public double ContainerLength { get; } + + /// + /// Gets the desired length of this result. + /// Just return this value as the desired size in . + /// public double DesiredLength { get; } + + /// + /// Gets the desired length if the container has infinite length. + /// public double GreedyDesiredLength { get; } + + /// + /// Contains the column/row calculation intermediate result. + /// This value is used by for reducing repeat calculation. + /// public IReadOnlyList LeanLengthList { get; } + + /// + /// Gets the length list for each column/row. + /// public IReadOnlyList LengthList { get; } } + /// + /// Stores the result of the measuring procedure. + /// This result can be used to arrange children and assign the render size. + /// internal class ArrangeResult { - public ArrangeResult(IReadOnlyList lengthList) + /// + /// Initialize a new instance of . + /// + internal ArrangeResult(IReadOnlyList lengthList) { LengthList = lengthList; } + /// + /// Gets the length list for each column/row. + /// public IReadOnlyList LengthList { get; } } } From ed01e59fc25efc3732869f25c616b7b92221f31b Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 10:44:40 +0800 Subject: [PATCH 16/38] Add document comment for some helper methods. --- src/Avalonia.Controls/Utils/GridLayout.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 93d83fe29a..9a886d60c9 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -195,7 +195,7 @@ namespace Avalonia.Controls.Utils shouldTestStarMin = @fixed; } - // M3/7. Fix all the Auto lengths that the children on its column/row have a zero or non-zero length. + // M4/7. Fix all the Auto lengths that the children on its column/row have a zero or non-zero length. // // +-----------------------------------------------------------+ // | * | A | * | P | A | * | P | * | * | @@ -307,19 +307,19 @@ namespace Avalonia.Controls.Utils } /// - /// + /// Use the to calculate the fixed length of the Auto column/row. /// - /// - /// - /// - /// + /// The convention list that has same length fixed. + /// The column/row index that should be fixed. + /// The unit * length for the current rest length. + /// The final length of the Auto length column/row. [Pure] private double ApplyAdditionalConventionsForAuto(IReadOnlyList conventions, int index, double starUnitLength) { // 1. Calculate all the * length with starUnitLength. // 2. Exclude all the fixed length and all the * length. - // 3. The rest of the desired length. + // 3. Compare the rest of the desired length and the convention. // +-----------------+ // | * | A | * | // +-----------------+ @@ -365,7 +365,7 @@ namespace Avalonia.Controls.Utils // TODO Calculate the min length of all the desired size. - return 150; + return 0; } /// From 4768124962baae85757fc32cc26bd210eae478ac Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 15:38:18 +0800 Subject: [PATCH 17/38] Fix the AggregateAdditionalConventionsForStars method. --- src/Avalonia.Controls/Utils/GridLayout.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 9a886d60c9..eba23a3d8e 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Layout; using JetBrains.Annotations; @@ -348,10 +349,19 @@ namespace Avalonia.Controls.Utils return Math.Min(conventions[index].MaxLength, more); } - [Pure] + /// + /// Calculate the total desired length of all the * length. + /// Bug Warning: + /// - The behavior of this method is undefined! Different UI Frameworks have different behaviors. + /// - We ignore all the span columns/rows and just take single cells into consideration. + /// + /// All the conventions that have almost been fixed except the rest *. + /// The total desired length of all the * length. + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] private double AggregateAdditionalConventionsForStars( IReadOnlyList conventions) { + // Note: The comment in this method is just a draft. // +-----------------------------------------------------------+ // | * | P | * | P | P | * | P | * | * | // +-----------------------------------------------------------+ @@ -361,11 +371,10 @@ namespace Avalonia.Controls.Utils // _additionalConventions 是上面的 x、y、z…… 集合,只有最小值是可用的。 // 需要返回所有标记为 * 的方格的累加和的最小值。 - var additionalConventions = _additionalConventions; - - // TODO Calculate the min length of all the desired size. - - return 0; + // Before we determine the behavior of this method, we just aggregate the one-span * columns. + return _additionalConventions + .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) + .Sum(x => x.Min); } /// From 2052d53d68b6f2a4a6d74128ff721ff7527869fb Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 16:19:49 +0800 Subject: [PATCH 18/38] Order the grid codes and make most unit test passed. --- src/Avalonia.Controls/Grid.cs | 443 ++---------------- src/Avalonia.Controls/Utils/GridLayout.cs | 22 +- .../Avalonia.Controls.UnitTests/GridMocks.cs | 1 + .../Avalonia.Controls.UnitTests/GridTests.cs | 25 +- 4 files changed, 80 insertions(+), 411 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index ed484ec378..8360b83fc4 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -48,10 +48,6 @@ namespace Avalonia.Controls private RowDefinitions _rowDefinitions; - private Segment[,] _rowMatrix; - - private Segment[,] _colMatrix; - /// /// Gets or sets the columns definitions for the grid. /// @@ -186,7 +182,16 @@ namespace Avalonia.Controls element.SetValue(RowSpanProperty, value); } + /// + /// Gets the result of last column measuring produce. + /// Use this result to reduce the arrange calculation. + /// private GridLayout.MeasureResult _columnMeasureCache; + + /// + /// Gets the result of last row measuring produce. + /// Use this result to reduce the arrange calculation. + /// private GridLayout.MeasureResult _rowMeasureCache; /// @@ -196,11 +201,32 @@ namespace Avalonia.Controls /// The desired size of the control. protected override Size MeasureOverride(Size constraint) { + // If the grid doesn't have any column/row definitions, it behaviors like a nomal panel. + + if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + { + var maxWidth = 0.0; + var maxHeight = 0.0; + foreach (var child in Children.OfType()) + { + child.Measure(constraint); + maxWidth = Math.Max(maxWidth, child.DesiredSize.Width); + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + } + + maxWidth = Math.Min(maxWidth, constraint.Width); + maxHeight = Math.Min(maxHeight, constraint.Height); + return new Size(maxWidth, maxHeight); + } + + // If the grid defines some columns or rows. + var measureCache = new Dictionary(); var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = new GridLayout(ColumnDefinitions); var rowLayout = new GridLayout(RowDefinitions); + // Note: If a child stays in a * or Auto column/row, use constraint to measure it. columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); @@ -221,6 +247,8 @@ namespace Avalonia.Controls _rowMeasureCache = rowResult; return new Size(columnResult.DesiredLength, rowResult.DesiredLength); + // Measure each child only once. + // If a child has been measured, it will just return the desired size. Size MeasureOnce(Control child, Size size) { if (measureCache.TryGetValue(child, out var desiredSize)) @@ -242,6 +270,20 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { + // If the grid doesn't have any column/row definitions, it behaviors like a nomal panel. + + if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + { + foreach (var child in Children.OfType()) + { + child.Arrange(new Rect(finalSize)); + } + + return finalSize; + } + + // If the grid defines some columns or rows. + var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = new GridLayout(ColumnDefinitions); @@ -263,6 +305,10 @@ namespace Avalonia.Controls return finalSize; } + /// + /// Get the safe column/columnspan and safe row/rowspan. + /// The result of this method ensure that none of the children has a column/row out of the definitions. + /// private (Dictionary safeColumns, Dictionary safeRows) GetSafeColumnRows() { @@ -301,22 +347,6 @@ namespace Avalonia.Controls return (index, span); } - private static double Clamp(double val, double min, double max) - { - if (val < min) - { - return min; - } - else if (val > max) - { - return max; - } - else - { - return val; - } - } - private static int ValidateColumn(AvaloniaObject o, int value) { if (value < 0) @@ -336,376 +366,5 @@ namespace Avalonia.Controls return value; } - - private void CreateMatrices(int rowCount, int colCount) - { - if (_rowMatrix == null || _colMatrix == null || - _rowMatrix.GetLength(0) != rowCount || - _colMatrix.GetLength(0) != colCount) - { - _rowMatrix = new Segment[rowCount, rowCount]; - _colMatrix = new Segment[colCount, colCount]; - } - else - { - Array.Clear(_rowMatrix, 0, _rowMatrix.Length); - Array.Clear(_colMatrix, 0, _colMatrix.Length); - } - } - - private void ExpandStarCols(Size availableSize) - { - int matrixCount = _colMatrix.GetLength(0); - int columnsCount = ColumnDefinitions.Count; - double width = availableSize.Width; - - for (int i = 0; i < matrixCount; i++) - { - if (_colMatrix[i, i].Type == GridUnitType.Star) - { - _colMatrix[i, i].OfferedSize = 0; - } - else - { - width = Math.Max(width - _colMatrix[i, i].OfferedSize, 0); - } - } - - AssignSize(_colMatrix, 0, matrixCount - 1, ref width, GridUnitType.Star, false); - width = Math.Max(0, width); - - if (columnsCount > 0) - { - for (int i = 0; i < matrixCount; i++) - { - if (_colMatrix[i, i].Type == GridUnitType.Star) - { - ColumnDefinitions[i].ActualWidth = _colMatrix[i, i].OfferedSize; - } - } - } - } - - private void ExpandStarRows(Size availableSize) - { - int matrixCount = _rowMatrix.GetLength(0); - int rowCount = RowDefinitions.Count; - double height = availableSize.Height; - - // When expanding star rows, we need to zero out their height before - // calling AssignSize. AssignSize takes care of distributing the - // available size when there are Mins and Maxs applied. - for (int i = 0; i < matrixCount; i++) - { - if (_rowMatrix[i, i].Type == GridUnitType.Star) - { - _rowMatrix[i, i].OfferedSize = 0.0; - } - else - { - height = Math.Max(height - _rowMatrix[i, i].OfferedSize, 0); - } - } - - AssignSize(_rowMatrix, 0, matrixCount - 1, ref height, GridUnitType.Star, false); - - if (rowCount > 0) - { - for (int i = 0; i < matrixCount; i++) - { - if (_rowMatrix[i, i].Type == GridUnitType.Star) - { - RowDefinitions[i].ActualHeight = _rowMatrix[i, i].OfferedSize; - } - } - } - } - - private void AssignSize( - Segment[,] matrix, - int start, - int end, - ref double size, - GridUnitType type, - bool desiredSize) - { - double count = 0; - bool assigned; - - // Count how many segments are of the correct type. If we're measuring Star rows/cols - // we need to count the number of stars instead. - for (int i = start; i <= end; i++) - { - double segmentSize = desiredSize ? matrix[i, i].DesiredSize : matrix[i, i].OfferedSize; - if (segmentSize < matrix[i, i].Max) - { - count += type == GridUnitType.Star ? matrix[i, i].Stars : 1; - } - } - - do - { - double contribution = size / count; - - assigned = false; - - for (int i = start; i <= end; i++) - { - double segmentSize = desiredSize ? matrix[i, i].DesiredSize : matrix[i, i].OfferedSize; - - if (!(matrix[i, i].Type == type && segmentSize < matrix[i, i].Max)) - { - continue; - } - - double newsize = segmentSize; - newsize += contribution * (type == GridUnitType.Star ? matrix[i, i].Stars : 1); - double newSizeIgnoringMinMax = newsize; - newsize = Math.Min(newsize, matrix[i, i].Max); - newsize = Math.Max(newsize, matrix[i, i].Min); - assigned |= !Equals(newsize, newSizeIgnoringMinMax); - size -= newsize - segmentSize; - - if (desiredSize) - { - matrix[i, i].DesiredSize = newsize; - } - else - { - matrix[i, i].OfferedSize = newsize; - } - } - } - while (assigned); - } - - private void AllocateDesiredSize(int rowCount, int colCount) - { - // First allocate the heights of the RowDefinitions, then allocate - // the widths of the ColumnDefinitions. - for (int i = 0; i < 2; i++) - { - Segment[,] matrix = i == 0 ? _rowMatrix : _colMatrix; - int count = i == 0 ? rowCount : colCount; - - for (int row = count - 1; row >= 0; row--) - { - for (int col = row; col >= 0; col--) - { - bool spansStar = false; - for (int j = row; j >= col; j--) - { - spansStar |= matrix[j, j].Type == GridUnitType.Star; - } - - // This is the amount of pixels which must be available between the grid rows - // at index 'col' and 'row'. i.e. if 'row' == 0 and 'col' == 2, there must - // be at least 'matrix [row][col].size' pixels of height allocated between - // all the rows in the range col -> row. - double current = matrix[row, col].DesiredSize; - - // Count how many pixels have already been allocated between the grid rows - // in the range col -> row. The amount of pixels allocated to each grid row/column - // is found on the diagonal of the matrix. - double totalAllocated = 0; - - for (int k = row; k >= col; k--) - { - totalAllocated += matrix[k, k].DesiredSize; - } - - // If the size requirement has not been met, allocate the additional required - // size between 'pixel' rows, then 'star' rows, finally 'auto' rows, until all - // height has been assigned. - if (totalAllocated < current) - { - double additional = current - totalAllocated; - - if (spansStar) - { - AssignSize(matrix, col, row, ref additional, GridUnitType.Star, true); - } - else - { - AssignSize(matrix, col, row, ref additional, GridUnitType.Pixel, true); - AssignSize(matrix, col, row, ref additional, GridUnitType.Auto, true); - } - } - } - } - } - - int rowMatrixDim = _rowMatrix.GetLength(0); - int colMatrixDim = _colMatrix.GetLength(0); - - for (int r = 0; r < rowMatrixDim; r++) - { - _rowMatrix[r, r].OfferedSize = _rowMatrix[r, r].DesiredSize; - } - - for (int c = 0; c < colMatrixDim; c++) - { - _colMatrix[c, c].OfferedSize = _colMatrix[c, c].DesiredSize; - } - } - - private void SaveMeasureResults() - { - int rowMatrixDim = _rowMatrix.GetLength(0); - int colMatrixDim = _colMatrix.GetLength(0); - - for (int i = 0; i < rowMatrixDim; i++) - { - for (int j = 0; j < rowMatrixDim; j++) - { - _rowMatrix[i, j].OriginalSize = _rowMatrix[i, j].OfferedSize; - } - } - - for (int i = 0; i < colMatrixDim; i++) - { - for (int j = 0; j < colMatrixDim; j++) - { - _colMatrix[i, j].OriginalSize = _colMatrix[i, j].OfferedSize; - } - } - } - - private void RestoreMeasureResults() - { - int rowMatrixDim = _rowMatrix.GetLength(0); - int colMatrixDim = _colMatrix.GetLength(0); - - for (int i = 0; i < rowMatrixDim; i++) - { - for (int j = 0; j < rowMatrixDim; j++) - { - _rowMatrix[i, j].OfferedSize = _rowMatrix[i, j].OriginalSize; - } - } - - for (int i = 0; i < colMatrixDim; i++) - { - for (int j = 0; j < colMatrixDim; j++) - { - _colMatrix[i, j].OfferedSize = _colMatrix[i, j].OriginalSize; - } - } - } - - /// - /// Stores the layout values of of of . - /// - private struct Segment - { - /// - /// Gets or sets the base size of this segment. - /// The value is from the user's code or from the stored measuring values. - /// - public double OriginalSize; - - /// - /// Gets the maximum size of this segment. - /// The value is from the user's code. - /// - public readonly double Max; - - /// - /// Gets the minimum size of this segment. - /// The value is from the user's code. - /// - public readonly double Min; - - /// - /// Gets or sets the row/column partial desired size of the . - /// - public double DesiredSize; - - /// - /// Gets or sets the row/column offered size that will be used to measure the children. - /// - public double OfferedSize; - - /// - /// Gets or sets the star unit size if the is . - /// - public double Stars; - - /// - /// Gets the segment size unit type. - /// - public readonly GridUnitType Type; - - public Segment(double offeredSize, double min, double max, GridUnitType type) - { - OriginalSize = 0; - Min = min; - Max = max; - DesiredSize = 0; - OfferedSize = offeredSize; - Stars = 0; - Type = type; - } - } - - private struct GridNode - { - public readonly int Row; - public readonly int Column; - public readonly double Size; - public readonly Segment[,] Matrix; - - public GridNode(Segment[,] matrix, int row, int col, double size) - { - Matrix = matrix; - Row = row; - Column = col; - Size = size; - } - } - - private class GridWalker - { - public GridWalker(Grid grid, Segment[,] rowMatrix, Segment[,] colMatrix) - { - int rowMatrixDim = rowMatrix.GetLength(0); - int colMatrixDim = colMatrix.GetLength(0); - - foreach (Control child in grid.Children) - { - bool starCol = false; - bool starRow = false; - bool autoCol = false; - bool autoRow = false; - - int col = Math.Min(GetColumn(child), colMatrixDim - 1); - int row = Math.Min(GetRow(child), rowMatrixDim - 1); - int colspan = Math.Min(GetColumnSpan(child), colMatrixDim - 1); - int rowspan = Math.Min(GetRowSpan(child), rowMatrixDim - 1); - - for (int r = row; r < row + rowspan; r++) - { - starRow |= rowMatrix[r, r].Type == GridUnitType.Star; - autoRow |= rowMatrix[r, r].Type == GridUnitType.Auto; - } - - for (int c = col; c < col + colspan; c++) - { - starCol |= colMatrix[c, c].Type == GridUnitType.Star; - autoCol |= colMatrix[c, c].Type == GridUnitType.Auto; - } - - HasAutoAuto |= autoRow && autoCol && !starRow && !starCol; - HasStarAuto |= starRow && autoCol; - HasAutoStar |= autoRow && starCol; - } - } - - public bool HasAutoAuto { get; } - - public bool HasStarAuto { get; } - - public bool HasAutoStar { get; } - } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index eba23a3d8e..7f5051475a 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -20,7 +20,9 @@ namespace Avalonia.Controls.Utils internal GridLayout([NotNull] ColumnDefinitions columns) { if (columns == null) throw new ArgumentNullException(nameof(columns)); - _conventions = columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); + _conventions = columns.Count == 0 + ? new List { new LengthConvention() } + : columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); } /// @@ -30,7 +32,9 @@ namespace Avalonia.Controls.Utils internal GridLayout([NotNull] RowDefinitions rows) { if (rows == null) throw new ArgumentNullException(nameof(rows)); - _conventions = rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); + _conventions = rows.Count == 0 + ? new List { new LengthConvention() } + : rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); } /// @@ -49,7 +53,8 @@ namespace Avalonia.Controls.Utils /// Gets all the length conventions that come from the grid children. /// [NotNull] - private readonly List _additionalConventions = new List(); + private readonly List _additionalConventions = + new List(); /// /// Some elements are not only in a single grid cell, they have one or more column/row spans, @@ -451,6 +456,7 @@ namespace Avalonia.Controls.Utils { return; } + var measureLength = 0.0; for (var i = 0; i < lengthList.Count; i++) { @@ -474,6 +480,16 @@ namespace Avalonia.Controls.Utils /// internal class LengthConvention : ICloneable { + /// + /// Initialize a new instance of . + /// + public LengthConvention() + { + Length = new GridLength(1.0, GridUnitType.Star); + MinLength = 0.0; + MaxLength = double.PositiveInfinity; + } + /// /// Initialize a new instance of . /// diff --git a/tests/Avalonia.Controls.UnitTests/GridMocks.cs b/tests/Avalonia.Controls.UnitTests/GridMocks.cs index a982882ad8..23b975207c 100644 --- a/tests/Avalonia.Controls.UnitTests/GridMocks.cs +++ b/tests/Avalonia.Controls.UnitTests/GridMocks.cs @@ -16,6 +16,7 @@ namespace Avalonia.Controls.UnitTests internal static Grid New(Size measure = default, Size arrange = default) { var grid = new Grid(); + grid.Children.Add(new Border()); grid.Measure(measure == default ? new Size(double.PositiveInfinity, double.PositiveInfinity) : measure); grid.Arrange(new Rect(default, arrange == default ? grid.DesiredSize : arrange)); return grid; diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 7d649a5a07..4c79b7775b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,13 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using Avalonia.Controls; using Xunit; namespace Avalonia.Controls.UnitTests @@ -72,26 +65,26 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Layout_PixelRowColumn_BoundsCorrect() + public void Layout_EmptyColumnRow_LayoutLikeANormalPanel() { // Arrange & Action - var rowGrid = GridMock.New(new RowDefinitions("100,200,300")); - var columnGrid = GridMock.New(new ColumnDefinitions("50,100,150")); + var grid = GridMock.New(arrange: new Size(600, 200)); // Assert - GridAssert.ChildrenHeight(rowGrid, 100, 200, 300); - GridAssert.ChildrenWidth(columnGrid, 50, 100, 150); + GridAssert.ChildrenWidth(grid, 600); + GridAssert.ChildrenHeight(grid, 200); } [Fact] - public void Layout_NoRowColumn_BoundsCorrect() + public void Layout_PixelRowColumn_BoundsCorrect() { // Arrange & Action - var grid = GridMock.New(arrange: new Size(600, 200)); + var rowGrid = GridMock.New(new RowDefinitions("100,200,300")); + var columnGrid = GridMock.New(new ColumnDefinitions("50,100,150")); // Assert - GridAssert.ChildrenHeight(grid, 600); - GridAssert.ChildrenWidth(grid, 200); + GridAssert.ChildrenHeight(rowGrid, 100, 200, 300); + GridAssert.ChildrenWidth(columnGrid, 50, 100, 150); } [Fact] From a6fe9928a1f76aff08a76fe2a043c57c7fb08d51 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 16:34:37 +0800 Subject: [PATCH 19/38] Make all unit test for grid pass. --- src/Avalonia.Controls/Grid.cs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 8360b83fc4..66e8295cb8 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -201,7 +201,10 @@ namespace Avalonia.Controls /// The desired size of the control. protected override Size MeasureOverride(Size constraint) { - // If the grid doesn't have any column/row definitions, it behaviors like a nomal panel. + // Situation 1/2: + // If the grid doesn't have any column/row definitions, + // it behaviors like a nomal panel. + // GridLayout supports this situation but we handle this separately for performance. if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) { @@ -219,6 +222,7 @@ namespace Avalonia.Controls return new Size(maxWidth, maxHeight); } + // Situation 2/2: // If the grid defines some columns or rows. var measureCache = new Dictionary(); @@ -270,7 +274,10 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { - // If the grid doesn't have any column/row definitions, it behaviors like a nomal panel. + // Situation 1/2: + // If the grid doesn't have any column/row definitions, + // it behaviors like a nomal panel. + // GridLayout supports this situation but we handle this separately for performance. if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) { @@ -282,10 +289,11 @@ namespace Avalonia.Controls return finalSize; } + // Situation 2/2: // If the grid defines some columns or rows. + // var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); var rowLayout = new GridLayout(RowDefinitions); @@ -296,10 +304,22 @@ namespace Avalonia.Controls { var (column, columnSpan) = safeColumns[child]; var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); - var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); + var x = Enumerable.Range(0, column).Sum(c => columnResult.LengthList[c]); + var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); + var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); + var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); + + child.Arrange(new Rect(x, y, width, height)); + } - child.Arrange(new Rect(0, 0, width, height)); + for (var i = 0; i < ColumnDefinitions.Count; i++) + { + ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; + } + + for (var i = 0; i < RowDefinitions.Count; i++) + { + RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; } return finalSize; From 382ef6e3968b226a09f363d56cbabf08015c1465 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 16:53:03 +0800 Subject: [PATCH 20/38] Add more comment for grid layout code. --- src/Avalonia.Controls/Grid.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 66e8295cb8..c8a459bb0c 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -227,16 +227,17 @@ namespace Avalonia.Controls var measureCache = new Dictionary(); var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); var rowLayout = new GridLayout(RowDefinitions); // Note: If a child stays in a * or Auto column/row, use constraint to measure it. columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); + // Calculate for measuring result. var columnResult = columnLayout.Measure(constraint.Width); var rowResult = rowLayout.Measure(constraint.Height); + // Use the measure result to measure the rest children. foreach (var child in Children.OfType()) { var (column, columnSpan) = safeColumns[child]; @@ -247,6 +248,7 @@ namespace Avalonia.Controls MeasureOnce(child, new Size(width, height)); } + // Cache the measure result and return the desired size. _columnMeasureCache = columnResult; _rowMeasureCache = rowResult; return new Size(columnResult.DesiredLength, rowResult.DesiredLength); @@ -292,14 +294,15 @@ namespace Avalonia.Controls // Situation 2/2: // If the grid defines some columns or rows. - // var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = new GridLayout(ColumnDefinitions); var rowLayout = new GridLayout(RowDefinitions); + // Calculate for arranging result. var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); + // Arrange the children. foreach (var child in Children.OfType()) { var (column, columnSpan) = safeColumns[child]; @@ -312,16 +315,19 @@ namespace Avalonia.Controls child.Arrange(new Rect(x, y, width, height)); } + // Assign the actual width. for (var i = 0; i < ColumnDefinitions.Count; i++) { ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; } + // Assign the actual height. for (var i = 0; i < RowDefinitions.Count; i++) { RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; } + // Return the render size. return finalSize; } From e280652922a8f6c391cb8186c320ebba296028bc Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 17:30:13 +0800 Subject: [PATCH 21/38] Add debugger display or the grid layout result. --- src/Avalonia.Controls/Utils/GridLayout.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 7f5051475a..e7a82addfd 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -593,6 +593,7 @@ namespace Avalonia.Controls.Utils /// This result can be used to measure children and assign the desired size. /// Passing this result to can reduce calculation. /// + [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] internal class MeasureResult { /// @@ -641,6 +642,7 @@ namespace Avalonia.Controls.Utils /// Stores the result of the measuring procedure. /// This result can be used to arrange children and assign the render size. /// + [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] internal class ArrangeResult { /// From 42bfe0bd4e05993c3eadf4a652a3e3d3618815aa Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 17:37:26 +0800 Subject: [PATCH 22/38] Mark Grid layout methods pure. --- src/Avalonia.Controls/Utils/GridLayout.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index e7a82addfd..6e38a7ceba 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls.Utils /// /// The measured result that containing the desired size and all the column/row length. /// - [NotNull] + [NotNull, Pure] internal MeasureResult Measure(double containerLength) { // Prepare all the variables that this method needs to use. @@ -290,7 +290,7 @@ namespace Avalonia.Controls.Utils /// /// The measured result that containing the desired size and all the column/row length. /// - [NotNull] + [NotNull, Pure] public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) { measure = measure ?? Measure(finalLength); From 3f1a5ed0093637759720a26aa4a94f7365197403 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 1 May 2018 18:56:20 +0800 Subject: [PATCH 23/38] Remove language usage condition. --- .../Avalonia.Controls.UnitTests.csproj | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 32fa6abe29..3706a50525 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,10 +1,8 @@  netcoreapp2.0 - Library - - latest + Library From 2af42057c17e03c1dc25fc6a60750d99e96f5f2f Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 4 May 2018 07:44:53 +0800 Subject: [PATCH 24/38] Fix the wrong Grid.GetSafeSpan index. --- src/Avalonia.Controls/Grid.cs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index c8a459bb0c..67b8ab4eda 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -340,6 +340,8 @@ namespace Avalonia.Controls { var columnCount = ColumnDefinitions.Count; var rowCount = RowDefinitions.Count; + columnCount = columnCount == 0 ? 1 : columnCount; + rowCount = rowCount == 0 ? 1 : rowCount; var safeColumns = Children.OfType().ToDictionary(child => child, child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); var safeRows = Children.OfType().ToDictionary(child => child, @@ -360,14 +362,26 @@ namespace Avalonia.Controls { var index = userIndex; var span = userSpan; - if (userIndex > length) + + if (index < 0) + { + span = index + span; + index = 0; + } + + if (span <= 0) + { + span = 1; + } + + if (userIndex >= length) { - index = length; + index = length - 1; span = 1; } else if (userIndex + userSpan > length) { - span = length - userIndex + 1; + span = length - userIndex; } return (index, span); From f334ef0be241030fb3f09be9a14c93752aae37ea Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 4 May 2018 17:27:46 +0800 Subject: [PATCH 25/38] Add debugger info for easier debug. --- src/Avalonia.Controls/Grid.cs | 6 ++++++ src/Avalonia.Controls/Utils/GridLayout.cs | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 67b8ab4eda..7892fdf352 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -224,6 +224,9 @@ namespace Avalonia.Controls // Situation 2/2: // If the grid defines some columns or rows. + // Debug Tip: + // - GridLayout doesn't hold any states, so you can drag the debugger execution + // arrow back to any statements and re-run them without any side-effect. var measureCache = new Dictionary(); var (safeColumns, safeRows) = GetSafeColumnRows(); @@ -293,6 +296,9 @@ namespace Avalonia.Controls // Situation 2/2: // If the grid defines some columns or rows. + // Debug Tip: + // - GridLayout doesn't hold any states, so you can drag the debugger execution + // arrow back to any statements and re-run them without any side-effect. var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = new GridLayout(ColumnDefinitions); diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 6e38a7ceba..cfbe81ba87 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Layout; @@ -478,6 +479,7 @@ namespace Avalonia.Controls.Utils /// This is mostly the same as or . /// We use this because we can treat the column and the row the same. /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] internal class LengthConvention : ICloneable { /// @@ -545,6 +547,12 @@ namespace Avalonia.Controls.Utils /// private bool _isFixed; + /// + /// Helps the debugger to display the intermediate column/row calculation result. + /// + private string DebuggerDisplay => + $"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; + /// object ICloneable.Clone() => Clone(); @@ -559,6 +567,7 @@ namespace Avalonia.Controls.Utils /// Contains the convention that comes from the grid children. /// Some child span multiple columns or rows, so even a simple column/row can have multiple conventions. /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] internal struct AdditionalLengthConvention { /// @@ -586,6 +595,12 @@ namespace Avalonia.Controls.Utils /// This value is usually provided by the child's desired length. /// public double Min { get; } + + /// + /// Helps the debugger to display the intermediate column/row calculation result. + /// + private string DebuggerDisplay => + $"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; } /// From a4cb49fa51c3a20406f71ab1490a1a3eecc769f9 Mon Sep 17 00:00:00 2001 From: walterlv Date: Fri, 4 May 2018 19:04:49 +0800 Subject: [PATCH 26/38] Fix the column/row span layout. --- src/Avalonia.Controls/Utils/GridLayout.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index cfbe81ba87..dd260f31df 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -337,8 +337,8 @@ namespace Avalonia.Controls.Utils 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) + // If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length. + if (index == additional.Index + additional.Span - 1) { var min = Enumerable.Range(additional.Index, additional.Span) .Select(x => From e58a0641bf842eab3777d2bae7b5cfb8f4d28d09 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 6 May 2018 07:57:00 +0800 Subject: [PATCH 27/38] If the children are in the same column/row, the Grid will not incorrectly aggregate their widths/heights. Instead, it will calculate the Max width/height. --- src/Avalonia.Controls/Utils/GridLayout.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index dd260f31df..53ca7b13c5 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -378,9 +378,10 @@ namespace Avalonia.Controls.Utils // 需要返回所有标记为 * 的方格的累加和的最小值。 // Before we determine the behavior of this method, we just aggregate the one-span * columns. - return _additionalConventions + var lookup = _additionalConventions .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) - .Sum(x => x.Min); + .ToLookup(x => x.Index); + return lookup.Select(group => group.Max(x => x.Min)).Sum(); } /// From b2a868dd3a15631b428aa5251c7b5334b503ed22 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 6 May 2018 09:35:36 +0800 Subject: [PATCH 28/38] Finish the AggregateAdditionalConventionsForStars method algorithm. --- src/Avalonia.Controls/Utils/GridLayout.cs | 40 ++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 53ca7b13c5..da2bf42f6d 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -367,21 +367,37 @@ namespace Avalonia.Controls.Utils private double AggregateAdditionalConventionsForStars( IReadOnlyList conventions) { - // Note: The comment in this method is just a draft. - // +-----------------------------------------------------------+ - // | * | P | * | P | P | * | P | * | * | - // +-----------------------------------------------------------+ - // |<- x ->| |<- z ->| - // |<- y ->| - // conveniences 是上面看到的那个列表,所有能够确定的 A、P 和 * 都已经转换成了 P;剩下的 * 只有 Max 是没确定的。 - // _additionalConventions 是上面的 x、y、z…… 集合,只有最小值是可用的。 - // 需要返回所有标记为 * 的方格的累加和的最小值。 + // 1. Determine all one-span column's desired widths or row's desired heights. + // 2. Order the multi-span conventions by its last index + // (Notice that the sorting data source is much smaller than the original children source.) + // 3. Determin each multi-span last index by calculating the maximun desired size. // Before we determine the behavior of this method, we just aggregate the one-span * columns. - var lookup = _additionalConventions + + var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); + + // Prepare a lengthList variable indicating the fixed length of each column/row. + var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); + foreach (var group in _additionalConventions .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) - .ToLookup(x => x.Index); - return lookup.Select(group => group.Max(x => x.Min)).Sum(); + .ToLookup(x => x.Index)) + { + lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); + } + + // Now the lengthList is fixed by every one-span columns/rows. + // Then we should determine the multi-span column's/row's length. + foreach (var group in _additionalConventions + .Where(x => x.Span > 1) + .ToLookup(x => x.Index + x.Span - 1) + // Order the multi-span columns/rows by last index. + .OrderBy(x => x.Key)) + { + var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); + lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); + } + + return lengthList.Sum() - fixedLength; } /// From 3453c6ed99b6b3081b150d603ea0d925f5611d15 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 6 May 2018 09:35:54 +0800 Subject: [PATCH 29/38] Fix the calendaritem style. --- src/Avalonia.Themes.Default/CalendarItem.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/CalendarItem.xaml b/src/Avalonia.Themes.Default/CalendarItem.xaml index a9756fdb5f..3d3d75a39a 100644 --- a/src/Avalonia.Themes.Default/CalendarItem.xaml +++ b/src/Avalonia.Themes.Default/CalendarItem.xaml @@ -27,7 +27,7 @@ - + From 03a97df75c02e2043f00c40e9ada54f85cef9bcd Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 6 May 2018 12:15:41 +0800 Subject: [PATCH 30/38] Fix some words spell. --- src/Avalonia.Controls/Grid.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7892fdf352..d1c65a273e 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -183,13 +183,13 @@ namespace Avalonia.Controls } /// - /// Gets the result of last column measuring produce. + /// Gets the result of last column measuring procedure. /// Use this result to reduce the arrange calculation. /// private GridLayout.MeasureResult _columnMeasureCache; /// - /// Gets the result of last row measuring produce. + /// Gets the result of last row measuring procedure. /// Use this result to reduce the arrange calculation. /// private GridLayout.MeasureResult _rowMeasureCache; From b09bd2f2f04dfac92e4575fbe3776edd41cef1c2 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 6 May 2018 14:10:04 +0800 Subject: [PATCH 31/38] Fix some spelling and grammar issues. --- src/Avalonia.Controls/Grid.cs | 27 ++++++++--------- src/Avalonia.Controls/Utils/GridLayout.cs | 37 ++++++++++++----------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index d1c65a273e..54fcefeb3f 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -183,13 +183,13 @@ namespace Avalonia.Controls } /// - /// Gets the result of last column measuring procedure. + /// Gets the result of the last column measurement. /// Use this result to reduce the arrange calculation. /// private GridLayout.MeasureResult _columnMeasureCache; /// - /// Gets the result of last row measuring procedure. + /// Gets the result of the last row measurement. /// Use this result to reduce the arrange calculation. /// private GridLayout.MeasureResult _rowMeasureCache; @@ -202,8 +202,7 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size constraint) { // Situation 1/2: - // If the grid doesn't have any column/row definitions, - // it behaviors like a nomal panel. + // If the grid doesn't have any column/row definitions, it behaves like a normal panel. // GridLayout supports this situation but we handle this separately for performance. if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) @@ -225,7 +224,7 @@ namespace Avalonia.Controls // Situation 2/2: // If the grid defines some columns or rows. // Debug Tip: - // - GridLayout doesn't hold any states, so you can drag the debugger execution + // - GridLayout doesn't hold any state, so you can drag the debugger execution // arrow back to any statements and re-run them without any side-effect. var measureCache = new Dictionary(); @@ -236,11 +235,11 @@ namespace Avalonia.Controls columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); - // Calculate for measuring result. + // Calculate measurement. var columnResult = columnLayout.Measure(constraint.Width); var rowResult = rowLayout.Measure(constraint.Height); - // Use the measure result to measure the rest children. + // Use the results of the measurement to measure the rest of the children. foreach (var child in Children.OfType()) { var (column, columnSpan) = safeColumns[child]; @@ -280,8 +279,7 @@ namespace Avalonia.Controls protected override Size ArrangeOverride(Size finalSize) { // Situation 1/2: - // If the grid doesn't have any column/row definitions, - // it behaviors like a nomal panel. + // If the grid doesn't have any column/row definitions, it behaves like a normal panel. // GridLayout supports this situation but we handle this separately for performance. if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) @@ -297,14 +295,14 @@ namespace Avalonia.Controls // Situation 2/2: // If the grid defines some columns or rows. // Debug Tip: - // - GridLayout doesn't hold any states, so you can drag the debugger execution + // - GridLayout doesn't hold any state, so you can drag the debugger execution // arrow back to any statements and re-run them without any side-effect. var (safeColumns, safeRows) = GetSafeColumnRows(); var columnLayout = new GridLayout(ColumnDefinitions); var rowLayout = new GridLayout(RowDefinitions); - // Calculate for arranging result. + // Calculate for arrange result. var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); @@ -339,8 +337,9 @@ namespace Avalonia.Controls /// /// Get the safe column/columnspan and safe row/rowspan. - /// The result of this method ensure that none of the children has a column/row out of the definitions. + /// This method ensures that none of the children has a column/row outside the bounds of the definitions. /// + [Pure] private (Dictionary safeColumns, Dictionary safeRows) GetSafeColumnRows() { @@ -357,9 +356,9 @@ namespace Avalonia.Controls /// /// Gets the safe row/column and rowspan/columnspan for a specified range. - /// The user may assign the row/column properties out of the row count or column cout, this method helps to keep them in. + /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. /// - /// The rows count or the columns count. + /// The row or column count. /// The row or column that the user assigned. /// The rowspan or columnspan that the user assigned. /// The safe row/column and rowspan/columnspan. diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index da2bf42f6d..1266afd2d4 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -16,7 +16,8 @@ namespace Avalonia.Controls.Utils { /// /// Initialize a new instance from the column definitions. - /// will forget that the layout data comes from the columns. + /// The instance doesn't care about whether the definitions are rows or columns. + /// It will not calculate the column or row differently. /// internal GridLayout([NotNull] ColumnDefinitions columns) { @@ -28,7 +29,8 @@ namespace Avalonia.Controls.Utils /// /// Initialize a new instance from the row definitions. - /// will forget that the layout data comes from the rows. + /// The instance doesn't care about whether the definitions are rows or columns. + /// It will not calculate the column or row differently. /// internal GridLayout([NotNull] RowDefinitions rows) { @@ -46,6 +48,7 @@ namespace Avalonia.Controls.Utils /// /// Gets all the length conventions that come from column/row definitions. /// These conventions provide limitations of each grid cell. + /// Limitations: the expected pixel length, the min/max pixel length, the * count. /// [NotNull] private readonly List _conventions; @@ -81,10 +84,10 @@ namespace Avalonia.Controls.Utils if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); // M1/7. Find all the Auto and * length columns/rows. - // Only these columns/rows' layout can be affected by the children desired size. + // Only these columns/rows' layout can be affected by the child desired size. // - // Find all columns/rows that has the length Auto or *. We'll measure the children in advance. - // Only these kind of columns/rows will affects the Grid layout. + // Find all columns/rows that have Auto or * length. We'll measure the children in advance. + // Only these kind of columns/rows will affect the Grid layout. // Please note: // - The columns/rows of Auto length will definitely be affected by the children size; // - However, only the Grid.DesiredSize can be affected by the *-length columns/rows unless the Grid has very much space (Infinitely). @@ -99,8 +102,8 @@ namespace Avalonia.Controls.Utils // 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。 // 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。 // 请注意: - // - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度; - // - 而对于 * 长度,一般只有 Grid.DesiredSize 会受到子元素布局影响,只有在 Grid 布局空间充足时行列长度才会被子元素布局影响。 + // - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize; + // - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。 // Find all the Auto and * length columns/rows. var found = new Dictionary(); @@ -138,7 +141,7 @@ namespace Avalonia.Controls.Utils /// The container length. Usually, it is the constraint of the method. /// /// - /// The measured result that containing the desired size and all the column/row length. + /// The measured result that containing the desired size and all the column/row lengths. /// [NotNull, Pure] internal MeasureResult Measure(double containerLength) @@ -149,7 +152,7 @@ namespace Avalonia.Controls.Utils var aggregatedLength = 0.0; double starUnitLength; - // M2/7. Aggregate all the pixel lengths. Then we can get the rest length by `containerLength - aggregatedLength`. + // M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`. // We mark the aggregated length as "fix" because we can completely determine their values. Same as below. // // +-----------------------------------------------------------+ @@ -176,7 +179,7 @@ namespace Avalonia.Controls.Utils while (shouldTestStarMin) { // Calculate the unit * length to estimate the length of each column/row that has * length. - // Under this estimated length, look for if there is a minimum value that has a length less than its constraint. + // Under this estimated length, check if there is a minimum value that has a length less than its constraint. // If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value. // // 计算单位 * 的长度,以便预估出每一个 * 行列的长度。 @@ -245,7 +248,7 @@ namespace Avalonia.Controls.Utils var desiredStarMin = AggregateAdditionalConventionsForStars(conventions); aggregatedLength += desiredStarMin; - // M6/7. Determine the desired length of the grid for current container length. Its value stores in desiredLength. + // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. // Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength. // // +-----------------------------------------------------------+ @@ -370,7 +373,7 @@ namespace Avalonia.Controls.Utils // 1. Determine all one-span column's desired widths or row's desired heights. // 2. Order the multi-span conventions by its last index // (Notice that the sorting data source is much smaller than the original children source.) - // 3. Determin each multi-span last index by calculating the maximun desired size. + // 3. Determine each multi-span last index by calculating the maximun desired size. // Before we determine the behavior of this method, we just aggregate the one-span * columns. @@ -401,10 +404,10 @@ namespace Avalonia.Controls.Utils } /// - /// This method implement the last procedure (M7/7) of measure. - /// It expand all the * length to the fixed length according to the . + /// This method implements the last procedure (M7/7) of measure. + /// It expands all the * length to the fixed length according to the . /// - /// All the conventions that have almost been fixed except the rest *. + /// All the conventions that have almost been fixed except the remaining *. /// The container length. /// The final pixel length list. [Pure] @@ -466,7 +469,7 @@ namespace Avalonia.Controls.Utils /// We should clip the columns/rows that have been out of the container bounds. /// Note: This method may change the items value of . /// - /// All the column width list or the row height list with fixed pixel length. + /// A list of all the column widths and row heights with a fixed pixel length /// the container length. It can be positive infinity. private static void Clip([NotNull] IList lengthList, double constraint) { @@ -582,7 +585,7 @@ namespace Avalonia.Controls.Utils /// /// Contains the convention that comes from the grid children. - /// Some child span multiple columns or rows, so even a simple column/row can have multiple conventions. + /// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions. /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] internal struct AdditionalLengthConvention From e4b9f9ff4071e33394ee4bb3c302306615db7ef3 Mon Sep 17 00:00:00 2001 From: walterlv Date: Sun, 6 May 2018 14:47:28 +0800 Subject: [PATCH 32/38] Fix more spelling and grammar issues. --- src/Avalonia.Controls/Utils/GridLayout.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 1266afd2d4..5f20e39ba9 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -89,8 +89,10 @@ namespace Avalonia.Controls.Utils // Find all columns/rows that have Auto or * length. We'll measure the children in advance. // Only these kind of columns/rows will affect the Grid layout. // Please note: - // - The columns/rows of Auto length will definitely be affected by the children size; - // - However, only the Grid.DesiredSize can be affected by the *-length columns/rows unless the Grid has very much space (Infinitely). + // - If the column / row has Auto length, the Grid.DesiredSize and the column width + // will be affected by the child's desired size. + // - If the column / row has* length, the Grid.DesiredSize will be affected by the + // child's desired size but the column width not. // +-----------------------------------------------------------+ // | * | A | * | P | A | * | P | * | * | @@ -205,7 +207,7 @@ namespace Avalonia.Controls.Utils shouldTestStarMin = @fixed; } - // M4/7. Fix all the Auto lengths that the children on its column/row have a zero or non-zero length. + // M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length. // // +-----------------------------------------------------------+ // | * | A | * | P | A | * | P | * | * | @@ -319,7 +321,7 @@ namespace Avalonia.Controls.Utils /// /// Use the to calculate the fixed length of the Auto column/row. /// - /// The convention list that has same length fixed. + /// The convention list that all the * with minimum length are fixed. /// The column/row index that should be fixed. /// The unit * length for the current rest length. /// The final length of the Auto length column/row. @@ -372,7 +374,7 @@ namespace Avalonia.Controls.Utils { // 1. Determine all one-span column's desired widths or row's desired heights. // 2. Order the multi-span conventions by its last index - // (Notice that the sorting data source is much smaller than the original children source.) + // (Notice that the sorted data is much smaller than the source.) // 3. Determine each multi-span last index by calculating the maximun desired size. // Before we determine the behavior of this method, we just aggregate the one-span * columns. @@ -542,8 +544,8 @@ namespace Avalonia.Controls.Utils internal double MaxLength { get; } /// - /// Fix the . If all columns/rows are fixed, - /// we can get the double pixel list of all columns/row. + /// Fix the . + /// If all columns/rows are fixed, we can get the size of all columns/rows in pixels. /// /// /// The pixel length that should be used to fix the convention. From 811e74062c43e423891ed263fe78f12c0905a8de Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 7 May 2018 19:32:09 +0800 Subject: [PATCH 33/38] Fix a comment so that is is more readable. --- src/Avalonia.Controls/Utils/GridLayout.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs index 5f20e39ba9..10a94a8c82 100644 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ b/src/Avalonia.Controls/Utils/GridLayout.cs @@ -47,8 +47,7 @@ namespace Avalonia.Controls.Utils /// /// Gets all the length conventions that come from column/row definitions. - /// These conventions provide limitations of each grid cell. - /// Limitations: the expected pixel length, the min/max pixel length, the * count. + /// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count. /// [NotNull] private readonly List _conventions; @@ -61,11 +60,13 @@ namespace Avalonia.Controls.Utils new List(); /// + /// Appending these elements into the convention list helps lay them out according to their desired sizes. + /// /// Some elements are not only in a single grid cell, they have one or more column/row spans, /// and these elements may affect the grid layout especially the measuring procedure. /// Append these elements into the convention list can help to layout them correctly through - /// their desired size. Only a small subset of grid children need to be measured before layout - /// starts and they will be called via the callback. + /// their desired size. Only a small subset of children need to be measured before layout starts + /// and they will be called via the callback. /// /// The grid children type. /// @@ -83,7 +84,7 @@ namespace Avalonia.Controls.Utils if (source == null) throw new ArgumentNullException(nameof(source)); if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); - // M1/7. Find all the Auto and * length columns/rows. + // M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.) // Only these columns/rows' layout can be affected by the child desired size. // // Find all columns/rows that have Auto or * length. We'll measure the children in advance. @@ -648,7 +649,7 @@ namespace Avalonia.Controls.Utils /// /// Gets the container length for this result. - /// This property exists because a measure result is related to it. + /// This property will be used by to determine whether to measure again or not. /// public double ContainerLength { get; } From 2a7d2cedc362e0b5ff50696f124f65e7b4b23054 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Tue, 8 May 2018 17:26:02 -0400 Subject: [PATCH 34/38] Fixes #1554 The issue was caused by the lack of a proper TextChanged event on the TextBox I implemented a work around --- src/Avalonia.Controls/AutoCompleteBox.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 8e801d606b..351cbf5520 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1954,6 +1954,10 @@ namespace Avalonia.Controls // 1. Minimum prefix length // 2. If a delay timer is in use, use it bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0; + if(populateReady && MinimumPrefixLength == 0 && String.IsNullOrEmpty(newText) && String.IsNullOrEmpty(SearchText)) + { + populateReady = false; + } _userCalledPopulate = populateReady ? userInitiated : false; // Update the interface and values only as necessary From a3e02e3068e672aee79ceb75aa8d21d6d2e08d92 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 9 May 2018 22:57:26 +0100 Subject: [PATCH 35/38] restore correct implementation of SetSystemDecorations on Win32. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index cf6cb40e58..8637f30970 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -245,19 +245,13 @@ namespace Avalonia.Win32 return; } - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); - - var systemDecorationStyles = UnmanagedMethods.WindowStyles.WS_OVERLAPPED - | UnmanagedMethods.WindowStyles.WS_CAPTION - | UnmanagedMethods.WindowStyles.WS_SYSMENU - | UnmanagedMethods.WindowStyles.WS_MINIMIZEBOX - | UnmanagedMethods.WindowStyles.WS_MAXIMIZEBOX; + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); - style |= systemDecorationStyles; + style |= UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW; if (!value) { - style ^= systemDecorationStyles; + style ^= UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW; } UnmanagedMethods.RECT windowRect; From 865a866388194ff08dcec1a131aca3ac3353eff1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 10 May 2018 13:23:49 +0100 Subject: [PATCH 36/38] update documentation and fix bug when system decorations are restored so that CanResize is obeyed. --- src/Avalonia.Controls/Window.cs | 4 ++- src/Windows/Avalonia.Win32/WindowImpl.cs | 35 ++++++++++++++++++------ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 16ee3a46b3..91d6c874d3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -214,7 +214,9 @@ namespace Avalonia.Controls } /// - /// Enables or disables resizing of the window + /// Enables or disables resizing of the window. + /// Note that if is set to False then this property + /// has no effect and should be treated as a recommendation for the user setting HasSystemDecorations. /// public bool CanResize { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8637f30970..be28b64c5a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -287,6 +287,21 @@ namespace Avalonia.Win32 UnmanagedMethods.SetWindowPosFlags.SWP_NOZORDER | UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); _decorated = value; + + if(_decorated) + { + if (_resizable) + { + // If we switch decorations back on we need to restore WS_SizeFrame. + _resizable = false; + CanResize(true); + } + else + { + _resizable = true; + CanResize(false); + } + } } public void Invalidate(Rect rect) @@ -857,14 +872,18 @@ namespace Avalonia.Win32 return; } - var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); - - if (value) - style |= UnmanagedMethods.WindowStyles.WS_SIZEFRAME; - else - style &= ~(UnmanagedMethods.WindowStyles.WS_SIZEFRAME); - - UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE, (uint)style); + if (_decorated) + { + var style = (UnmanagedMethods.WindowStyles)UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE); + + if (value) + style |= UnmanagedMethods.WindowStyles.WS_SIZEFRAME; + else + style &= ~(UnmanagedMethods.WindowStyles.WS_SIZEFRAME); + + UnmanagedMethods.SetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE, (uint)style); + } + _resizable = value; } } From 400dc55071ff2a5ae23f116c07380f3fedb0a843 Mon Sep 17 00:00:00 2001 From: zii-dmg Date: Fri, 11 May 2018 18:28:39 +0300 Subject: [PATCH 37/38] Fixed HRESULT size (long -> uint) --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5c24aa1c69..86dcec410b 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1187,27 +1187,21 @@ namespace Avalonia.Win32.Interop [Flags] public enum OpenFileNameFlags { - OFN_ALLOWMULTISELECT = 0x00000200, - OFN_EXPLORER = 0x00080000, - OFN_HIDEREADONLY = 0x00000004, - OFN_NOREADONLYRETURN = 0x00008000, - OFN_OVERWRITEPROMPT = 0x00000002 - } - public enum HRESULT : long + public enum HRESULT : uint { S_FALSE = 0x0001, S_OK = 0x0000, E_INVALIDARG = 0x80070057, E_OUTOFMEMORY = 0x8007000E, E_NOTIMPL = 0x80004001, - E_UNEXPECTED = 0x8000FFFF, + E_UNEXPECTED = 0x8000FFFF } public enum Icons From c2005c870232256a9471fc4f0e037ffe63a3ed1f Mon Sep 17 00:00:00 2001 From: zii-dmg Date: Sun, 13 May 2018 13:07:04 +0300 Subject: [PATCH 38/38] OleInitialize checks already inited case and throws Win32Exception --- src/Windows/Avalonia.Win32/OleContext.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index 085c0f8ea9..d454c797fa 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading; using Avalonia.Platform; using Avalonia.Threading; @@ -26,8 +27,11 @@ namespace Avalonia.Win32 private OleContext() { - if (UnmanagedMethods.OleInitialize(IntPtr.Zero) != UnmanagedMethods.HRESULT.S_OK) - throw new SystemException("Failed to initialize OLE"); + UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero); + + if (res != UnmanagedMethods.HRESULT.S_OK && + res != UnmanagedMethods.HRESULT.S_FALSE /*already initialized*/) + throw new Win32Exception((int)res, "Failed to initialize OLE"); } private static bool IsValidOleThread()