From e403299bb2179437c1d184a9b2377b7ee456da66 Mon Sep 17 00:00:00 2001 From: walterlv Date: Mon, 30 Apr 2018 13:31:23 +0800 Subject: [PATCH] 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); + } } }