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); element.SetValue(RowSpanProperty, value);
} }
private GridLayout.MeasureResult _columnMeasureCache;
private GridLayout.MeasureResult _rowMeasureCache;
/// <summary> /// <summary>
/// Measures the grid. /// Measures the grid.
/// </summary> /// </summary>
@ -193,97 +196,45 @@ namespace Avalonia.Controls
/// <returns>The desired size of the control.</returns> /// <returns>The desired size of the control.</returns>
protected override Size MeasureOverride(Size constraint) 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 columnLayout = new GridLayout(ColumnDefinitions);
var rowLayout = new GridLayout(RowDefinitions); var rowLayout = new GridLayout(RowDefinitions);
var autoSizeColumns = columnLayout.Prepare(); columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width);
var autoSizeRows = rowLayout.Prepare(); rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height);
foreach (var pair in safeColumns)
{
var child = pair.Key;
var (column, columnSpan) = pair.Value;
var columnLast = column + columnSpan - 1;
if (autoSizeColumns.Contains(columnLast))
{
}
}
// Calculate row height list and column width list. var columnResult = columnLayout.Measure(constraint.Width);
var widthList = columnLayout.Measure(constraint.Width); var rowResult = rowLayout.Measure(constraint.Height);
var heightList = rowLayout.Measure(constraint.Height);
// Calculate the available width list and height list for every child. foreach (var child in Children.OfType<Control>())
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 =>
{ {
var (row, rowSpan) = GetSafeSpan(heightList.Count, GetRow(child), GetRowSpan(child)); var (column, columnSpan) = safeColumns[child];
return Span(heightList, row, rowSpan).Sum(); 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. MeasureOnce(child, new Size(width, height));
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)));
var currentDesiredWidth = 0.0; _columnMeasureCache = columnResult;
var currentDesiredHeight = 0.0; _rowMeasureCache = rowResult;
var currentColumn = 0; return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
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;
if (column == currentColumn) Size MeasureOnce(Control child, Size size)
{ {
currentDesiredWidth = Math.Max(desiredSize.Width, currentDesiredWidth); if (measureCache.TryGetValue(child, out var desiredSize))
}
else
{
currentDesiredWidth = desiredSize.Width;
currentColumn = column;
availableWidth -= desiredSize.Width;
desiredHeight -= desiredSize.Width;
}
if (availableWidth < desiredSize.Width)
{ {
return desiredSize;
} }
availableHeight -= desiredSize.Height; child.Measure(size);
desiredHeight -= desiredSize.Width; desiredSize = child.DesiredSize;
measureCache[child] = desiredSize;
return desiredSize;
} }
return constraint;
} }
/// <summary> /// <summary>
@ -293,25 +244,39 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns> /// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
// Calculate row height list and column width list. var (safeColumns, safeRows) = GetSafeColumnRows();
var rowLayout = new GridLayout(RowDefinitions);
var columnLayout = new GridLayout(ColumnDefinitions); var columnLayout = new GridLayout(ColumnDefinitions);
var heightList = rowLayout.Measure(finalSize.Height); var rowLayout = new GridLayout(RowDefinitions);
var widthList = columnLayout.Measure(finalSize.Width);
var rowMeasure = new Dictionary<Control, (double row, double rowspan)>(); var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
var columnMeasure = new Dictionary<Control, (double column, double columnspan)>(); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
foreach (var child in Children.OfType<Control>().OrderBy(GetRow))
{ foreach (var child in Children.OfType<Control>())
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 (column, columnSpan) = GetSafeSpan(widthList.Count, GetColumn(child), GetColumnSpan(child)); var (column, columnSpan) = safeColumns[child];
columnMeasure.Add(child, (column, columnSpan)); 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> /// <summary>
@ -340,32 +305,6 @@ namespace Avalonia.Controls
return (index, span); 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) private static double Clamp(double val, double min, double max)
{ {
if (val < min) if (val < min)

425
src/Avalonia.Controls/Utils/GridLayout.cs

@ -1,263 +1,328 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Avalonia.Controls.Utils 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 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> /// <summary>
/// Find out which rows/columns should be measured first. These rows/columns are those that marked with "Auto" size.<para/> /// Some elements are not in a single grid cell, they have multiple column/row spans,
/// These "Auto" size rows/columns behavior like fix-size rows/columns but they can only be determined after Measure. /// 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> /// </summary>
/// <returns>The row/column numbers that should be Measure first.</returns> /// <typeparam name="T"></typeparam>
internal List<int> Prepare() /// <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; // M1/6. Find all the Auto length columns/rows.
return Find().ToList(); // Only these columns/rows' layout can be affected by the children desired size.
var found = new Dictionary<T, (int index, int span)>();
IEnumerable<int> Find() 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; foreach (var pair in source.Where(x =>
if (unitType == GridUnitType.Auto) x.Value.index <= index && index < x.Value.index + x.Value.span))
{ {
yield return i; found[pair.Key] = pair.Value;
} }
} }
} }
}
/// <summary> // Append these layout into the additional convention list.
/// Try to calculate the lengths that will be used to measure the children.<para/> foreach (var pair in found)
/// 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))
{ {
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));
}
} }
}
// --- internal MeasureResult Measure(double containerLength)
// Warning! The code below will start to change the lengths item value. {
// --- // Initial.
var conventions = _conventions.Select(x => x.Clone()).ToList();
// Exclude the star unit if its min/max length range does not contain the calculated star length. var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value);
var intermediateStarLengths = lengths.Where(x => x.Length.IsStar).ToList(); var constraint = containerLength;
// Indicate whether all star lengths are in range of min and max or not. double starUnitLength;
var allInRange = false;
while (!allInRange) // 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) = (convention.Length.Value, convention.MinLength);
var (star, min, max) = (length.Length.Value, length.MinLength, length.MaxLength);
var starLength = star * starUnitLength; 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. convention.Fix(min);
if (starLength < min) starLength = min;
{ constraint -= starLength;
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; starCount -= star;
starUnitLength = containerLength / starCount; @fixed = true;
break; break;
} }
} }
// All lengths are in range, so that we have enough lengths to measure children. shouldTestStarMin = @fixed;
allInRange = true; }
foreach (var length in intermediateStarLengths)
// 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. // M5/6. Determine the desired length of the grid for current contaienr length. Its value stores in desiredLength.
return lengths.Select(x => // But if the container has infinite length, the grid desired length is stored in greedyDesiredLength.
x.Length.GridUnitType == GridUnitType.Auto var desiredLength = constraint >= 0.0 ? containerLength - constraint : containerLength;
? double.PositiveInfinity var greedyDesiredLength = containerLength - constraint;
: x.Length.Value).ToList();
// 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> public ArrangeResult Arrange(double finalLength, MeasureResult measure)
/// 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)
{ {
var lengths = _lengths.Clone(); // If the arrange final length does not equal to the measure length, we should measure again.
if (finalLength - measure.ContainerLength > LayoutTolerance)
// Exclude all the pixel lengths, so that we can calculate the star lengths. {
containerLength -= lengths // If the final length is larger, we will rerun the whole measure.
.Where(x => x.Length.IsAbsolute) measure = Measure(finalLength);
.Aggregate(0.0, (sum, add) => sum + add.Length.Value); }
else if (finalLength - measure.ContainerLength < -LayoutTolerance)
// 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(); // 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);
} }
// --- return new ArrangeResult(measure.LengthList);
// Warning! The code below will start to change the lengths item value. }
// ---
[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. // M6/6.
var intermediateStarLengths = lengths.Where(x => x.Length.IsStar).ToList(); if (constraint >= 0)
// Indicate whether all star lengths are in range of min and max or not.
var allInRange = false;
while (!allInRange)
{ {
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 @fixed = false;
var (star, min, max) = (length.Length.Value, length.MinLength, length.MaxLength); starUnitLength = constraint / starCount;
var starLength = star * starUnitLength; foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength)))
if (starLength < min || starLength > max)
{ {
// If the star length is out of min to max, change it to a pixel unit. var (star, max) = (convention.Length.Value, convention.MaxLength);
if (starLength < min) var starLength = star * starUnitLength;
{ if (starLength > max)
length.Update(min);
starLength = min;
}
else if (starLength > max)
{ {
length.Update(max); convention.Fix(max);
starLength = 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. shouldTestStarMax = @fixed;
allInRange = true;
foreach (var length in intermediateStarLengths)
{
length.Update(length.Length.Value * starUnitLength);
} }
} }
// Return the modified lengths as measuring lengths. foreach (var convention in dynamicConvention.Where(x => x.Length.IsStar))
return lengths.Select(x => {
x.Length.GridUnitType == GridUnitType.Auto convention.Fix(starUnitLength * convention.Length.Value);
? double.PositiveInfinity }
: x.Length.Value).ToList();
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; public LengthConvention(GridLength length, double minLength, double maxLength)
private LengthDefinitions(IEnumerable<LengthDefinition> lengths)
{ {
_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(); object ICloneable.Clone() => Clone();
public LengthDefinitions Clone() => new LengthDefinitions( internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength);
_lengths.Select(x => new LengthDefinition(x.Length, x.MinLength, x.MaxLength))); }
public static implicit operator LengthDefinitions(RowDefinitions rows) internal struct AdditionalLengthConvention
=> new LengthDefinitions(rows.Select(x => (LengthDefinition) x)); {
public int Index { get; }
public int Span { get; }
public double Min { get; }
public static implicit operator LengthDefinitions(ColumnDefinitions rows) public AdditionalLengthConvention(int index, int span, double min)
=> new LengthDefinitions(rows.Select(x => (LengthDefinition)x)); {
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; ContainerLength = containerLength;
MinLength = minLength; DesiredLength = desiredLength;
MaxLength = maxLength; GreedyDesiredLength = greedyDesiredLength;
LeanLengthList = leanConventions;
LengthList = expandedConventions.Select(x => x.Length.Value).ToList();
} }
internal GridLength Length { get; private set; } public double ContainerLength { get; }
internal double MinLength { get; } public double DesiredLength { get; }
internal double MaxLength { get; } public double GreedyDesiredLength { get; }
public IReadOnlyList<LengthConvention> LeanLengthList { get; }
public static implicit operator LengthDefinition(RowDefinition row) public IReadOnlyList<double> LengthList { get; }
=> 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) 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; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
{ {
public class GridLayoutTests public class GridLayoutTests
{ {
[Fact] [Theory]
public void Measure_AllPixelLength_Correct() [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 TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
var layout = new GridLayout(new RowDefinitions("100,200,300"));
// Action
var measure = layout.Measure(800);
// Assert
Assert.Equal(measure, new [] { 100d, 200d, 300d });
} }
[Fact] [Theory]
public void Measure_AllStarLength_Correct() [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })]
public void MeasureArrange_AllStarLength_Correct(string length, double containerLength,
double expectedDesiredLength, IList<double> expectedLengthList)
{ {
// Arrange TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
var layout = new GridLayout(new RowDefinitions("*,2*,3*"));
// Action
var measure = layout.Measure(600);
// Assert
Assert.Equal(measure, new [] { 100d, 200d, 300d });
} }
[Fact] [Theory]
public void Measure_MixStarPixelLength_Correct() [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 TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
var layout = new GridLayout(new RowDefinitions("100,2*,3*"));
// Action
var measure = layout.Measure(600);
// Assert
Assert.Equal(measure, new [] { 100d, 200d, 300d });
} }
[Fact] [Theory]
public void Measure_MixAutoPixelLength_Correct() [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 TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
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] [Theory]
public void Measure_MixAutoStarLength_Correct() [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })]
public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength,
double expectedDesiredLength, IList<double> expectedLengthList)
{ {
// Arrange TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
var layout = new GridLayout(new RowDefinitions("*,2*,Auto"));
// Action
var measure = layout.Measure(600);
// Assert
Assert.Equal(measure, new[] { 200d, 400d, double.PositiveInfinity });
} }
[Fact] [Theory]
public void Measure_MixAutoStarPixelLength_Correct() [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })]
public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength,
double expectedDesiredLength, IList<double> expectedLengthList)
{ {
// Arrange TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
var layout = new GridLayout(new RowDefinitions("*,200,Auto"));
// Action
var measure = layout.Measure(600);
// Assert
Assert.Equal(measure, new[] { 400d, 200d, double.PositiveInfinity });
} }
[Fact] [Fact]
public void Measure_AllPixelLengthButNotEnough_Correct() public void MeasureArrange_AllPixelLengthButNotEnough_Correct()
{ {
// Arrange // Arrange
var layout = new GridLayout(new RowDefinitions("100,200,300")); var layout = new GridLayout(new RowDefinitions("100,200,300"));
// Action // Measure - Action & Assert
var measure = layout.Measure(400); var measure = layout.Measure(400);
Assert.Equal(new[] { 100d, 200d, 300d }, measure.LengthList);
// Assert // Arrange - Action & Assert
Assert.Equal(measure, new[] { 100d, 200d, 300d });
} }
//[Fact] [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")]
//public void Arrange_AllPixelLength_Correct() private static void TestRowDefinitionsOnly(string length, double containerLength,
//{ double expectedDesiredLength, IList<double> expectedLengthList)
// // Arrange {
// var layout = new GridLayout(new RowDefinitions("100,200,300")); // Arrange
var layout = new GridLayout(new RowDefinitions(length));
// // 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 // Measure - Action & Assert
// var arrange = layout.Arrange(400); var measure = layout.Measure(containerLength);
Assert.Equal(expectedDesiredLength, measure.DesiredLength);
Assert.Equal(expectedLengthList, measure.LengthList);
// // Assert // Arrange - Action & Assert
// Assert.Equal(arrange, new[] { 100d, 200d, 300d }); var arrange = layout.Arrange(containerLength, measure);
//} Assert.Equal(expectedLengthList, arrange.LengthList);
}
} }
} }

Loading…
Cancel
Save