Browse Source

Use a new algorithm to layout Grid.

pull/1517/head
walterlv 9 years ago
parent
commit
e403299bb2
  1. 175
      src/Avalonia.Controls/Grid.cs
  2. 425
      src/Avalonia.Controls/Utils/GridLayout.cs
  3. 204
      tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs

175
src/Avalonia.Controls/Grid.cs

@ -186,6 +186,9 @@ namespace Avalonia.Controls
element.SetValue(RowSpanProperty, value);
}
private GridLayout.MeasureResult _columnMeasureCache;
private GridLayout.MeasureResult _rowMeasureCache;
/// <summary>
/// Measures the grid.
/// </summary>
@ -193,97 +196,45 @@ namespace Avalonia.Controls
/// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size constraint)
{
// +------- 1. Prepare the children status -------+
// + +
// +------- ------------------------------ -------+
var measureCache = new Dictionary<Control, Size>();
var (safeColumns, safeRows) = GetSafeColumnRows();
// Normalize the column/columnspan and row/rowspan.
var columnCount = ColumnDefinitions.Count;
var rowCount = RowDefinitions.Count;
var safeColumns = Children.OfType<Control>().ToDictionary(child => child,
child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child)));
var safeRows = Children.OfType<Control>().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<Control>().ToDictionary(child => child, child =>
{
var (column, columnSpan) = GetSafeSpan(widthList.Count, GetColumn(child), GetColumnSpan(child));
return Span(widthList, column, columnSpan).Sum();
});
var childrenAvailableHeights = Children.OfType<Control>().ToDictionary(child => child, child =>
foreach (var child in Children.OfType<Control>())
{
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<Control>()
.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;
}
/// <summary>
@ -293,25 +244,39 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
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<Control, (double row, double rowspan)>();
var columnMeasure = new Dictionary<Control, (double column, double columnspan)>();
foreach (var child in Children.OfType<Control>().OrderBy(GetRow))
{
var (row, rowSpan) = GetSafeSpan(heightList.Count, GetRow(child), GetRowSpan(child));
rowMeasure.Add(child, (row, rowSpan));
}
foreach (var child in Children.OfType<Control>().OrderBy(GetColumn))
var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
foreach (var child in Children.OfType<Control>())
{
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<Control, (int index, int span)> safeColumns,
Dictionary<Control, (int index, int span)> safeRows) GetSafeColumnRows()
{
var columnCount = ColumnDefinitions.Count;
var rowCount = RowDefinitions.Count;
var safeColumns = Children.OfType<Control>().ToDictionary(child => child,
child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child)));
var safeRows = Children.OfType<Control>().ToDictionary(child => child,
child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child)));
return (safeColumns, safeRows);
}
/// <summary>
@ -340,32 +305,6 @@ namespace Avalonia.Controls
return (index, span);
}
/// <summary>
/// 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.
/// </summary>
[Pure]
private static IEnumerable<double> Span(IList<double> 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)

425
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.
//
/// <summary>
/// Contains algorithms that can help to measure and arrange a Grid.
/// </summary>
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<LengthConvention> _conventions;
private readonly List<AdditionalLengthConvention> _additionalConventions = new List<AdditionalLengthConvention>();
/// <summary>
/// Find out which rows/columns should be measured first. These rows/columns are those that marked with "Auto" size.<para/>
/// 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.<para/>
/// 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 <paramref name="getDesiredLength"/> callback.
/// </summary>
/// <returns>The row/column numbers that should be Measure first.</returns>
internal List<int> Prepare()
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="getDesiredLength"></param>
internal void AppendMeasureConventions<T>(IDictionary<T, (int index, int span)> source,
Func<T, double> getDesiredLength)
{
var lengths = _lengths;
return Find().ToList();
IEnumerable<int> 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<T, (int index, int span)>();
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;
}
}
}
}
/// <summary>
/// Try to calculate the lengths that will be used to measure the children.<para/>
/// If the <paramref name="containerLength"/> is not enough, we'll even not compress the measure length.
/// So you'd better call <see cref="Prepare"/> first to find out the rows/columns that should be excluded first.
/// </summary>
/// <param name="containerLength">
/// The container length (width or height) excluding some rows/columns.
/// Call <see cref="Prepare"/> first to find out the rows/columns that should be excluded.
/// </param>
/// <returns>The lengths that can be used to measure the children.</returns>
[Pure]
internal List<double> 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);
}
/// <summary>
/// Try to calculate the lengths that will be used to measure the children.
/// If the <paramref name="containerLength"/> is not enough, we'll even not compress the measure length.
/// </summary>
/// <param name="containerLength">The container length, width or height.</param>
/// <returns>The lengths that can be used to measure the children.</returns>
[Pure]
internal List<double> 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<LengthConvention> ExpandStars(IEnumerable<LengthConvention> 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<LengthDefinition>, ICloneable
internal class LengthConvention : ICloneable
{
private readonly List<LengthDefinition> _lengths;
private LengthDefinitions(IEnumerable<LengthDefinition> 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<LengthDefinition> 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<LengthConvention> leanConventions, IReadOnlyList<LengthConvention> 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<LengthConvention> LeanLengthList { get; }
public IReadOnlyList<double> LengthList { get; }
}
public void Update(double pixel)
internal class ArrangeResult
{
public ArrangeResult(IReadOnlyList<double> lengthList)
{
Length = new GridLength(pixel);
LengthList = lengthList;
}
public IReadOnlyList<double> LengthList { get; }
}
}
}

204
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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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);
}
}
}

Loading…
Cancel
Save