6 changed files with 2555 additions and 4696 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,705 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics; |
|
||||
using System.Globalization; |
|
||||
using System.Linq; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
using Avalonia.Layout; |
|
||||
using JetBrains.Annotations; |
|
||||
|
|
||||
namespace Avalonia.Controls.Utils |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Contains algorithms that can help to measure and arrange a Grid.
|
|
||||
/// </summary>
|
|
||||
internal class GridLayout |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initialize a new <see cref="GridLayout"/> instance from the column definitions.
|
|
||||
/// The instance doesn't care about whether the definitions are rows or columns.
|
|
||||
/// It will not calculate the column or row differently.
|
|
||||
/// </summary>
|
|
||||
internal GridLayout([NotNull] ColumnDefinitions columns) |
|
||||
{ |
|
||||
if (columns == null) throw new ArgumentNullException(nameof(columns)); |
|
||||
_conventions = columns.Count == 0 |
|
||||
? new List<LengthConvention> { new LengthConvention() } |
|
||||
: columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initialize a new <see cref="GridLayout"/> instance from the row definitions.
|
|
||||
/// The instance doesn't care about whether the definitions are rows or columns.
|
|
||||
/// It will not calculate the column or row differently.
|
|
||||
/// </summary>
|
|
||||
internal GridLayout([NotNull] RowDefinitions rows) |
|
||||
{ |
|
||||
if (rows == null) throw new ArgumentNullException(nameof(rows)); |
|
||||
_conventions = rows.Count == 0 |
|
||||
? new List<LengthConvention> { new LengthConvention() } |
|
||||
: rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same.
|
|
||||
/// </summary>
|
|
||||
private const double LayoutTolerance = 1.0 / 256.0; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets all the length conventions that come from column/row definitions.
|
|
||||
/// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count.
|
|
||||
/// </summary>
|
|
||||
[NotNull] |
|
||||
private readonly List<LengthConvention> _conventions; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets all the length conventions that come from the grid children.
|
|
||||
/// </summary>
|
|
||||
[NotNull] |
|
||||
private readonly List<AdditionalLengthConvention> _additionalConventions = |
|
||||
new List<AdditionalLengthConvention>(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Appending these elements into the convention list helps lay them out according to their desired sizes.
|
|
||||
/// <para/>
|
|
||||
/// Some elements are not only in a single grid cell, they have one or more column/row spans,
|
|
||||
/// and these elements may affect the grid layout especially the measuring procedure.<para/>
|
|
||||
/// Append these elements into the convention list can help to layout them correctly through
|
|
||||
/// their desired size. Only a small subset of children need to be measured before layout starts
|
|
||||
/// and they will be called via the<paramref name="getDesiredLength"/> callback.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="T">The grid children type.</typeparam>
|
|
||||
/// <param name="source">
|
|
||||
/// Contains the safe column/row index and its span.
|
|
||||
/// Notice that we will not verify whether the range is in the column/row count,
|
|
||||
/// so you should get the safe column/row info first.
|
|
||||
/// </param>
|
|
||||
/// <param name="getDesiredLength">
|
|
||||
/// This callback will be called if the <see cref="GridLayout"/> thinks that a child should be
|
|
||||
/// measured first. Usually, these are the children that have the * or Auto length.
|
|
||||
/// </param>
|
|
||||
internal void AppendMeasureConventions<T>([NotNull] IDictionary<T, (int index, int span)> source, |
|
||||
[NotNull] Func<T, double> getDesiredLength) |
|
||||
{ |
|
||||
if (source == null) throw new ArgumentNullException(nameof(source)); |
|
||||
if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); |
|
||||
|
|
||||
// M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.)
|
|
||||
// Only these columns/rows' layout can be affected by the child desired size.
|
|
||||
//
|
|
||||
// Find all columns/rows that have Auto or * length. We'll measure the children in advance.
|
|
||||
// Only these kind of columns/rows will affect the Grid layout.
|
|
||||
// Please note:
|
|
||||
// - If the column / row has Auto length, the Grid.DesiredSize and the column width
|
|
||||
// will be affected by the child's desired size.
|
|
||||
// - If the column / row has* length, the Grid.DesiredSize will be affected by the
|
|
||||
// child's desired size but the column width not.
|
|
||||
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// _conventions: | min | max | | | min | | min max | max |
|
|
||||
// _additionalC: |<- desired ->| |< desired >|
|
|
||||
// _additionalC: |< desired >| |<- desired ->|
|
|
||||
|
|
||||
// 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。
|
|
||||
// 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。
|
|
||||
// 请注意:
|
|
||||
// - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize;
|
|
||||
// - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。
|
|
||||
|
|
||||
// Find all the Auto and * length columns/rows.
|
|
||||
var found = new Dictionary<T, (int index, int span)>(); |
|
||||
for (var i = 0; i < _conventions.Count; i++) |
|
||||
{ |
|
||||
var index = i; |
|
||||
var convention = _conventions[index]; |
|
||||
if (convention.Length.IsAuto || convention.Length.IsStar) |
|
||||
{ |
|
||||
foreach (var pair in source.Where(x => |
|
||||
x.Value.index <= index && index < x.Value.index + x.Value.span)) |
|
||||
{ |
|
||||
found[pair.Key] = pair.Value; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Append these layout into the additional convention list.
|
|
||||
foreach (var pair in found) |
|
||||
{ |
|
||||
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)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Run measure procedure according to the <paramref name="containerLength"/> and gets the <see cref="MeasureResult"/>.
|
|
||||
/// </summary>
|
|
||||
/// <param name="containerLength">
|
|
||||
/// The container length. Usually, it is the constraint of the <see cref="Layoutable.MeasureOverride"/> method.
|
|
||||
/// </param>
|
|
||||
/// <param name="conventions">
|
|
||||
/// Overriding conventions that allows the algorithm to handle external inputa
|
|
||||
/// </param>
|
|
||||
/// <returns>
|
|
||||
/// The measured result that containing the desired size and all the column/row lengths.
|
|
||||
/// </returns>
|
|
||||
[NotNull, Pure] |
|
||||
internal MeasureResult Measure(double containerLength, IReadOnlyList<LengthConvention> conventions = null) |
|
||||
{ |
|
||||
// Prepare all the variables that this method needs to use.
|
|
||||
conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); |
|
||||
var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); |
|
||||
var aggregatedLength = 0.0; |
|
||||
double starUnitLength; |
|
||||
|
|
||||
// M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`.
|
|
||||
// We mark the aggregated length as "fix" because we can completely determine their values. Same as below.
|
|
||||
//
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// |#fix#| |#fix#|
|
|
||||
//
|
|
||||
// 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。
|
|
||||
// 我们会将所有能够确定下长度的行列标记为 fix。下同。
|
|
||||
// 请注意:
|
|
||||
// - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。
|
|
||||
|
|
||||
aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); |
|
||||
|
|
||||
// M3/7. Fix all the * lengths that have reached the minimum.
|
|
||||
//
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | min | max | | | min | | min max | max |
|
|
||||
// | fix | |#fix#| fix |
|
|
||||
|
|
||||
var shouldTestStarMin = true; |
|
||||
while (shouldTestStarMin) |
|
||||
{ |
|
||||
// Calculate the unit * length to estimate the length of each column/row that has * length.
|
|
||||
// Under this estimated length, check if there is a minimum value that has a length less than its constraint.
|
|
||||
// If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value.
|
|
||||
//
|
|
||||
// 计算单位 * 的长度,以便预估出每一个 * 行列的长度。
|
|
||||
// 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。
|
|
||||
// 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。
|
|
||||
var @fixed = false; |
|
||||
starUnitLength = (containerLength - aggregatedLength) / starCount; |
|
||||
foreach (var convention in conventions.Where(x => x.Length.IsStar)) |
|
||||
{ |
|
||||
var (star, min) = (convention.Length.Value, convention.MinLength); |
|
||||
var starLength = star * starUnitLength; |
|
||||
if (starLength < min) |
|
||||
{ |
|
||||
convention.Fix(min); |
|
||||
starLength = min; |
|
||||
aggregatedLength += starLength; |
|
||||
starCount -= star; |
|
||||
@fixed = true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
shouldTestStarMin = @fixed; |
|
||||
} |
|
||||
|
|
||||
// M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length.
|
|
||||
//
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | min | max | | | min | | min max | max |
|
|
||||
// |#fix#| | fix |#fix#| fix | fix |
|
|
||||
|
|
||||
var shouldTestAuto = true; |
|
||||
while (shouldTestAuto) |
|
||||
{ |
|
||||
var @fixed = false; |
|
||||
starUnitLength = (containerLength - aggregatedLength) / starCount; |
|
||||
for (var i = 0; i < conventions.Count; i++) |
|
||||
{ |
|
||||
var convention = conventions[i]; |
|
||||
if (!convention.Length.IsAuto) |
|
||||
{ |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); |
|
||||
convention.Fix(more); |
|
||||
aggregatedLength += more; |
|
||||
@fixed = true; |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
shouldTestAuto = @fixed; |
|
||||
} |
|
||||
|
|
||||
// M5/7. Expand the stars according to the additional conventions (usually the child desired length).
|
|
||||
// We can't fix this kind of length, so we just mark them as desired (des).
|
|
||||
//
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | min | max | | | min | | min max | max |
|
|
||||
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
|
|
||||
|
|
||||
var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); |
|
||||
aggregatedLength += desiredStarMin; |
|
||||
|
|
||||
// M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength.
|
|
||||
// Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength.
|
|
||||
//
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | min | max | | | min | | min max | max |
|
|
||||
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
|
|
||||
// Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure.
|
|
||||
//
|
|
||||
// desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des)
|
|
||||
// greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des
|
|
||||
|
|
||||
var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; |
|
||||
var greedyDesiredLength = aggregatedLength; |
|
||||
|
|
||||
// M7/7. Expand all the rest stars. These stars have no conventions or only have
|
|
||||
// max value they can be expanded from zero to constraint.
|
|
||||
//
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | * | A | * | P | A | * | P | * | * |
|
|
||||
// +-----------------------------------------------------------+
|
|
||||
// | min | max | | | min | | min max | max |
|
|
||||
// |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#|
|
|
||||
// Note: This table will be stored as the final result into the MeasureResult.
|
|
||||
|
|
||||
var dynamicConvention = ExpandStars(conventions, containerLength); |
|
||||
Clip(dynamicConvention, containerLength); |
|
||||
|
|
||||
// Returns the measuring result.
|
|
||||
return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, |
|
||||
conventions, dynamicConvention, minLengths); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Run arrange procedure according to the <paramref name="measure"/> and gets the <see cref="ArrangeResult"/>.
|
|
||||
/// </summary>
|
|
||||
/// <param name="finalLength">
|
|
||||
/// The container length. Usually, it is the finalSize of the <see cref="Layoutable.ArrangeOverride"/> method.
|
|
||||
/// </param>
|
|
||||
/// <param name="measure">
|
|
||||
/// The result that the measuring procedure returns. If it is null, a new measure procedure will run.
|
|
||||
/// </param>
|
|
||||
/// <returns>
|
|
||||
/// The measured result that containing the desired size and all the column/row length.
|
|
||||
/// </returns>
|
|
||||
[NotNull, Pure] |
|
||||
public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) |
|
||||
{ |
|
||||
measure = measure ?? Measure(finalLength); |
|
||||
|
|
||||
// If the arrange final length does not equal to the measure length, we should measure again.
|
|
||||
if (finalLength - measure.ContainerLength > LayoutTolerance) |
|
||||
{ |
|
||||
// If the final length is larger, we will rerun the whole measure.
|
|
||||
measure = Measure(finalLength, measure.LeanLengthList); |
|
||||
} |
|
||||
else if (finalLength - measure.ContainerLength < -LayoutTolerance) |
|
||||
{ |
|
||||
// If the final length is smaller, we measure the M6/6 procedure only.
|
|
||||
var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); |
|
||||
measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, |
|
||||
measure.LeanLengthList, dynamicConvention, measure.MinLengths); |
|
||||
} |
|
||||
|
|
||||
return new ArrangeResult(measure.LengthList); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Use the <see cref="_additionalConventions"/> to calculate the fixed length of the Auto column/row.
|
|
||||
/// </summary>
|
|
||||
/// <param name="conventions">The convention list that all the * with minimum length are fixed.</param>
|
|
||||
/// <param name="index">The column/row index that should be fixed.</param>
|
|
||||
/// <param name="starUnitLength">The unit * length for the current rest length.</param>
|
|
||||
/// <returns>The final length of the Auto length column/row.</returns>
|
|
||||
[Pure] |
|
||||
private double ApplyAdditionalConventionsForAuto(IReadOnlyList<LengthConvention> conventions, |
|
||||
int index, double starUnitLength) |
|
||||
{ |
|
||||
// 1. Calculate all the * length with starUnitLength.
|
|
||||
// 2. Exclude all the fixed length and all the * length.
|
|
||||
// 3. Compare the rest of the desired length and the convention.
|
|
||||
// +-----------------+
|
|
||||
// | * | A | * |
|
|
||||
// +-----------------+
|
|
||||
// | exl | | exl |
|
|
||||
// |< desired >|
|
|
||||
// |< desired >|
|
|
||||
|
|
||||
var more = 0.0; |
|
||||
foreach (var additional in _additionalConventions) |
|
||||
{ |
|
||||
// If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length.
|
|
||||
if (index == additional.Index + additional.Span - 1) |
|
||||
{ |
|
||||
var min = Enumerable.Range(additional.Index, additional.Span) |
|
||||
.Select(x => |
|
||||
{ |
|
||||
var c = conventions[x]; |
|
||||
if (c.Length.IsAbsolute) return c.Length.Value; |
|
||||
if (c.Length.IsStar) return c.Length.Value * starUnitLength; |
|
||||
return 0.0; |
|
||||
}).Sum(); |
|
||||
more = Math.Max(additional.Min - min, more); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return Math.Min(conventions[index].MaxLength, more); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Calculate the total desired length of all the * length.
|
|
||||
/// Bug Warning:
|
|
||||
/// - The behavior of this method is undefined! Different UI Frameworks have different behaviors.
|
|
||||
/// - We ignore all the span columns/rows and just take single cells into consideration.
|
|
||||
/// </summary>
|
|
||||
/// <param name="conventions">All the conventions that have almost been fixed except the rest *.</param>
|
|
||||
/// <returns>The total desired length of all the * length.</returns>
|
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] |
|
||||
private (List<double>, double) AggregateAdditionalConventionsForStars( |
|
||||
IReadOnlyList<LengthConvention> conventions) |
|
||||
{ |
|
||||
// 1. Determine all one-span column's desired widths or row's desired heights.
|
|
||||
// 2. Order the multi-span conventions by its last index
|
|
||||
// (Notice that the sorted data is much smaller than the source.)
|
|
||||
// 3. Determine each multi-span last index by calculating the maximum desired size.
|
|
||||
|
|
||||
// Before we determine the behavior of this method, we just aggregate the one-span * columns.
|
|
||||
|
|
||||
var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); |
|
||||
|
|
||||
// Prepare a lengthList variable indicating the fixed length of each column/row.
|
|
||||
var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); |
|
||||
foreach (var group in _additionalConventions |
|
||||
.Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) |
|
||||
.ToLookup(x => x.Index)) |
|
||||
{ |
|
||||
lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); |
|
||||
} |
|
||||
|
|
||||
// Now the lengthList is fixed by every one-span columns/rows.
|
|
||||
// Then we should determine the multi-span column's/row's length.
|
|
||||
foreach (var group in _additionalConventions |
|
||||
.Where(x => x.Span > 1) |
|
||||
.ToLookup(x => x.Index + x.Span - 1) |
|
||||
// Order the multi-span columns/rows by last index.
|
|
||||
.OrderBy(x => x.Key)) |
|
||||
{ |
|
||||
var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); |
|
||||
lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); |
|
||||
} |
|
||||
|
|
||||
return (lengthList, lengthList.Sum() - fixedLength); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// This method implements the last procedure (M7/7) of measure.
|
|
||||
/// It expands all the * length to the fixed length according to the <paramref name="constraint"/>.
|
|
||||
/// </summary>
|
|
||||
/// <param name="conventions">All the conventions that have almost been fixed except the remaining *.</param>
|
|
||||
/// <param name="constraint">The container length.</param>
|
|
||||
/// <returns>The final pixel length list.</returns>
|
|
||||
[Pure] |
|
||||
private static List<double> 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; |
|
||||
|
|
||||
// M6/6.
|
|
||||
if (constraint >= 0) |
|
||||
{ |
|
||||
var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); |
|
||||
|
|
||||
var shouldTestStarMax = true; |
|
||||
while (shouldTestStarMax) |
|
||||
{ |
|
||||
var @fixed = false; |
|
||||
starUnitLength = constraint / starCount; |
|
||||
foreach (var convention in dynamicConvention.Where(x => |
|
||||
x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) |
|
||||
{ |
|
||||
var (star, max) = (convention.Length.Value, convention.MaxLength); |
|
||||
var starLength = star * starUnitLength; |
|
||||
if (starLength > max) |
|
||||
{ |
|
||||
convention.Fix(max); |
|
||||
starLength = max; |
|
||||
constraint -= starLength; |
|
||||
starCount -= star; |
|
||||
@fixed = true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
shouldTestStarMax = @fixed; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto)); |
|
||||
|
|
||||
var starUnit = starUnitLength; |
|
||||
var result = dynamicConvention.Select(x => |
|
||||
{ |
|
||||
if (x.Length.IsStar) |
|
||||
{ |
|
||||
return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value; |
|
||||
} |
|
||||
|
|
||||
return x.Length.Value; |
|
||||
}).ToList(); |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// If the container length is not infinity. It may be not enough to contain all the columns/rows.
|
|
||||
/// We should clip the columns/rows that have been out of the container bounds.
|
|
||||
/// Note: This method may change the items value of <paramref name="lengthList"/>.
|
|
||||
/// </summary>
|
|
||||
/// <param name="lengthList">A list of all the column widths and row heights with a fixed pixel length</param>
|
|
||||
/// <param name="constraint">the container length. It can be positive infinity.</param>
|
|
||||
private static void Clip([NotNull] IList<double> lengthList, double constraint) |
|
||||
{ |
|
||||
if (double.IsInfinity(constraint)) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var measureLength = 0.0; |
|
||||
for (var i = 0; i < lengthList.Count; i++) |
|
||||
{ |
|
||||
var length = lengthList[i]; |
|
||||
if (constraint - measureLength > length) |
|
||||
{ |
|
||||
measureLength += length; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
lengthList[i] = constraint - measureLength; |
|
||||
measureLength = constraint; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Contains the convention of each column/row.
|
|
||||
/// This is mostly the same as <see cref="RowDefinition"/> or <see cref="ColumnDefinition"/>.
|
|
||||
/// We use this because we can treat the column and the row the same.
|
|
||||
/// </summary>
|
|
||||
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] |
|
||||
internal class LengthConvention : ICloneable |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initialize a new instance of <see cref="LengthConvention"/>.
|
|
||||
/// </summary>
|
|
||||
public LengthConvention() |
|
||||
{ |
|
||||
Length = new GridLength(1.0, GridUnitType.Star); |
|
||||
MinLength = 0.0; |
|
||||
MaxLength = double.PositiveInfinity; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initialize a new instance of <see cref="LengthConvention"/>.
|
|
||||
/// </summary>
|
|
||||
public LengthConvention(GridLength length, double minLength, double maxLength) |
|
||||
{ |
|
||||
Length = length; |
|
||||
MinLength = minLength; |
|
||||
MaxLength = maxLength; |
|
||||
if (length.IsAbsolute) |
|
||||
{ |
|
||||
_isFixed = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the <see cref="GridLength"/> of a column or a row.
|
|
||||
/// </summary>
|
|
||||
internal GridLength Length { get; private set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the minimum convention for a column or a row.
|
|
||||
/// </summary>
|
|
||||
internal double MinLength { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the maximum convention for a column or a row.
|
|
||||
/// </summary>
|
|
||||
internal double MaxLength { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Fix the <see cref="LengthConvention"/>.
|
|
||||
/// If all columns/rows are fixed, we can get the size of all columns/rows in pixels.
|
|
||||
/// </summary>
|
|
||||
/// <param name="pixel">
|
|
||||
/// The pixel length that should be used to fix the convention.
|
|
||||
/// </param>
|
|
||||
/// <exception cref="InvalidOperationException">
|
|
||||
/// If the convention is pixel length, this exception will throw.
|
|
||||
/// </exception>
|
|
||||
public void Fix(double pixel) |
|
||||
{ |
|
||||
if (_isFixed) |
|
||||
{ |
|
||||
throw new InvalidOperationException("Cannot fix the length convention if it is fixed."); |
|
||||
} |
|
||||
|
|
||||
Length = new GridLength(pixel); |
|
||||
_isFixed = true; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value that indicates whether this convention is fixed.
|
|
||||
/// </summary>
|
|
||||
private bool _isFixed; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Helps the debugger to display the intermediate column/row calculation result.
|
|
||||
/// </summary>
|
|
||||
private string DebuggerDisplay => |
|
||||
$"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; |
|
||||
|
|
||||
/// <inheritdoc />
|
|
||||
object ICloneable.Clone() => Clone(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Get a deep copy of this convention list.
|
|
||||
/// We need this because we want to store some intermediate states.
|
|
||||
/// </summary>
|
|
||||
internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Contains the convention that comes from the grid children.
|
|
||||
/// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions.
|
|
||||
/// </summary>
|
|
||||
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] |
|
||||
internal struct AdditionalLengthConvention |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initialize a new instance of <see cref="AdditionalLengthConvention"/>.
|
|
||||
/// </summary>
|
|
||||
public AdditionalLengthConvention(int index, int span, double min) |
|
||||
{ |
|
||||
Index = index; |
|
||||
Span = span; |
|
||||
Min = min; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the start index of this additional convention.
|
|
||||
/// </summary>
|
|
||||
public int Index { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the span of this additional convention.
|
|
||||
/// </summary>
|
|
||||
public int Span { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the minimum length of this additional convention.
|
|
||||
/// This value is usually provided by the child's desired length.
|
|
||||
/// </summary>
|
|
||||
public double Min { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Helps the debugger to display the intermediate column/row calculation result.
|
|
||||
/// </summary>
|
|
||||
private string DebuggerDisplay => |
|
||||
$"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Stores the result of the measuring procedure.
|
|
||||
/// This result can be used to measure children and assign the desired size.
|
|
||||
/// Passing this result to <see cref="Arrange"/> can reduce calculation.
|
|
||||
/// </summary>
|
|
||||
[DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] |
|
||||
internal class MeasureResult |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initialize a new instance of <see cref="MeasureResult"/>.
|
|
||||
/// </summary>
|
|
||||
internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, |
|
||||
IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions, IReadOnlyList<double> minLengths) |
|
||||
{ |
|
||||
ContainerLength = containerLength; |
|
||||
DesiredLength = desiredLength; |
|
||||
GreedyDesiredLength = greedyDesiredLength; |
|
||||
LeanLengthList = leanConventions; |
|
||||
LengthList = expandedConventions; |
|
||||
MinLengths = minLengths; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the container length for this result.
|
|
||||
/// This property will be used by <see cref="Arrange"/> to determine whether to measure again or not.
|
|
||||
/// </summary>
|
|
||||
public double ContainerLength { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the desired length of this result.
|
|
||||
/// Just return this value as the desired size in <see cref="Layoutable.MeasureOverride"/>.
|
|
||||
/// </summary>
|
|
||||
public double DesiredLength { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the desired length if the container has infinite length.
|
|
||||
/// </summary>
|
|
||||
public double GreedyDesiredLength { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Contains the column/row calculation intermediate result.
|
|
||||
/// This value is used by <see cref="Arrange"/> for reducing repeat calculation.
|
|
||||
/// </summary>
|
|
||||
public IReadOnlyList<LengthConvention> LeanLengthList { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the length list for each column/row.
|
|
||||
/// </summary>
|
|
||||
public IReadOnlyList<double> LengthList { get; } |
|
||||
public IReadOnlyList<double> MinLengths { get; } |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Stores the result of the measuring procedure.
|
|
||||
/// This result can be used to arrange children and assign the render size.
|
|
||||
/// </summary>
|
|
||||
[DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] |
|
||||
internal class ArrangeResult |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initialize a new instance of <see cref="ArrangeResult"/>.
|
|
||||
/// </summary>
|
|
||||
internal ArrangeResult(IReadOnlyList<double> lengthList) |
|
||||
{ |
|
||||
LengthList = lengthList; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the length list for each column/row.
|
|
||||
/// </summary>
|
|
||||
public IReadOnlyList<double> LengthList { get; } |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,651 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Collections.Specialized; |
|
||||
using System.ComponentModel; |
|
||||
using System.Diagnostics; |
|
||||
using System.Linq; |
|
||||
using System.Reactive.Disposables; |
|
||||
using System.Reactive.Subjects; |
|
||||
using Avalonia.Collections; |
|
||||
using Avalonia.Controls.Utils; |
|
||||
using Avalonia.Layout; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Controls |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Shared size scope implementation.
|
|
||||
/// Shares the size information between participating grids.
|
|
||||
/// An instance of this class is attached to every <see cref="Control"/> that has its
|
|
||||
/// IsSharedSizeScope property set to true.
|
|
||||
/// </summary>
|
|
||||
internal sealed class SharedSizeScopeHost : IDisposable |
|
||||
{ |
|
||||
private enum MeasurementState |
|
||||
{ |
|
||||
Invalidated, |
|
||||
Measuring, |
|
||||
Cached |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Class containing the measured rows/columns for a single grid.
|
|
||||
/// Monitors changes to the row/column collections as well as the SharedSizeGroup changes
|
|
||||
/// for the individual items in those collections.
|
|
||||
/// Notifies the <see cref="SharedSizeScopeHost"/> of SharedSizeGroup changes.
|
|
||||
/// </summary>
|
|
||||
private sealed class MeasurementCache : IDisposable |
|
||||
{ |
|
||||
readonly CompositeDisposable _subscriptions; |
|
||||
readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); |
|
||||
|
|
||||
public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; |
|
||||
|
|
||||
public MeasurementCache(Grid grid) |
|
||||
{ |
|
||||
Grid = grid; |
|
||||
Results = grid.RowDefinitions.Cast<DefinitionBase>() |
|
||||
.Concat(grid.ColumnDefinitions) |
|
||||
.Select(d => new MeasurementResult(grid, d)) |
|
||||
.ToList(); |
|
||||
|
|
||||
grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; |
|
||||
grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; |
|
||||
|
|
||||
|
|
||||
_subscriptions = new CompositeDisposable( |
|
||||
Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), |
|
||||
Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), |
|
||||
grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), |
|
||||
grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); |
|
||||
|
|
||||
} |
|
||||
|
|
||||
// method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid
|
|
||||
private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
|
||||
{ |
|
||||
// route to collection changed as a Reset.
|
|
||||
DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); |
|
||||
} |
|
||||
|
|
||||
private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> propertyChanged) |
|
||||
{ |
|
||||
if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) |
|
||||
{ |
|
||||
var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); |
|
||||
var oldName = result.SizeGroup?.Name; |
|
||||
var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; |
|
||||
_groupChanged.OnNext((oldName, newName, result)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|
||||
{ |
|
||||
int offset = 0; |
|
||||
if (sender is ColumnDefinitions) |
|
||||
offset = Grid.RowDefinitions.Count; |
|
||||
|
|
||||
var newItems = e.NewItems?.OfType<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>(); |
|
||||
var oldItems = e.OldStartingIndex >= 0 |
|
||||
? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) |
|
||||
: new List<MeasurementResult>(); |
|
||||
|
|
||||
void NotifyNewItems() |
|
||||
{ |
|
||||
foreach (var item in newItems) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) |
|
||||
continue; |
|
||||
|
|
||||
_groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
void NotifyOldItems() |
|
||||
{ |
|
||||
foreach (var item in oldItems) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) |
|
||||
continue; |
|
||||
|
|
||||
_groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
switch (e.Action) |
|
||||
{ |
|
||||
case NotifyCollectionChangedAction.Add: |
|
||||
Results.InsertRange(e.NewStartingIndex + offset, newItems); |
|
||||
NotifyNewItems(); |
|
||||
break; |
|
||||
|
|
||||
case NotifyCollectionChangedAction.Remove: |
|
||||
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); |
|
||||
NotifyOldItems(); |
|
||||
break; |
|
||||
|
|
||||
case NotifyCollectionChangedAction.Move: |
|
||||
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); |
|
||||
Results.InsertRange(e.NewStartingIndex + offset, oldItems); |
|
||||
break; |
|
||||
|
|
||||
case NotifyCollectionChangedAction.Replace: |
|
||||
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); |
|
||||
Results.InsertRange(e.NewStartingIndex + offset, newItems); |
|
||||
|
|
||||
NotifyOldItems(); |
|
||||
NotifyNewItems(); |
|
||||
|
|
||||
break; |
|
||||
|
|
||||
case NotifyCollectionChangedAction.Reset: |
|
||||
oldItems = Results; |
|
||||
newItems = Results = Grid.RowDefinitions.Cast<DefinitionBase>() |
|
||||
.Concat(Grid.ColumnDefinitions) |
|
||||
.Select(d => new MeasurementResult(Grid, d)) |
|
||||
.ToList(); |
|
||||
NotifyOldItems(); |
|
||||
NotifyNewItems(); |
|
||||
|
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Updates the Results collection with Grid Measure results.
|
|
||||
/// </summary>
|
|
||||
/// <param name="rowResult">Result of the GridLayout.Measure method for the RowDefinitions in the grid.</param>
|
|
||||
/// <param name="columnResult">Result of the GridLayout.Measure method for the ColumnDefinitions in the grid.</param>
|
|
||||
public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
|
||||
{ |
|
||||
MeasurementState = MeasurementState.Cached; |
|
||||
for (int i = 0; i < Grid.RowDefinitions.Count; i++) |
|
||||
{ |
|
||||
Results[i].MeasuredResult = rowResult.LengthList[i]; |
|
||||
Results[i].MinLength = rowResult.MinLengths[i]; |
|
||||
} |
|
||||
|
|
||||
for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) |
|
||||
{ |
|
||||
Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; |
|
||||
Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clears the measurement cache, in preparation for the Measure pass.
|
|
||||
/// </summary>
|
|
||||
public void InvalidateMeasure() |
|
||||
{ |
|
||||
var newItems = new List<MeasurementResult>(); |
|
||||
var oldItems = new List<MeasurementResult>(); |
|
||||
|
|
||||
MeasurementState = MeasurementState.Invalidated; |
|
||||
|
|
||||
Results.ForEach(r => |
|
||||
{ |
|
||||
r.MeasuredResult = double.NaN; |
|
||||
r.SizeGroup?.Reset(); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clears the <see cref="IObservable{T}"/> subscriptions.
|
|
||||
/// </summary>
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_subscriptions.Dispose(); |
|
||||
_groupChanged.OnCompleted(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the <see cref="Grid"/> for which this cache has been created.
|
|
||||
/// </summary>
|
|
||||
public Grid Grid { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the <see cref="MeasurementState"/> of this cache.
|
|
||||
/// </summary>
|
|
||||
public MeasurementState MeasurementState { get; private set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the list of <see cref="MeasurementResult"/> instances.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions
|
|
||||
/// </remarks>
|
|
||||
public List<MeasurementResult> Results { get; private set; } |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Class containing the Measure result for a single Row/Column in a grid.
|
|
||||
/// </summary>
|
|
||||
private class MeasurementResult |
|
||||
{ |
|
||||
public MeasurementResult(Grid owningGrid, DefinitionBase definition) |
|
||||
{ |
|
||||
OwningGrid = owningGrid; |
|
||||
Definition = definition; |
|
||||
MeasuredResult = double.NaN; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the <see cref="RowDefinition"/>/<see cref="ColumnDefinition"/> related to this <see cref="MeasurementResult"/>
|
|
||||
/// </summary>
|
|
||||
public DefinitionBase Definition { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the actual result of the Measure operation for this column.
|
|
||||
/// </summary>
|
|
||||
public double MeasuredResult { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids.
|
|
||||
/// </summary>
|
|
||||
public double MinLength { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the <see cref="Group"/> that this result belongs to.
|
|
||||
/// </summary>
|
|
||||
public Group SizeGroup { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the Grid that is the parent of the Row/Column
|
|
||||
/// </summary>
|
|
||||
public Grid OwningGrid { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup.
|
|
||||
/// </summary>
|
|
||||
/// <returns>A tuple of length and the priority in the shared size group.</returns>
|
|
||||
public (double length, int priority) GetPriorityLength() |
|
||||
{ |
|
||||
var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; |
|
||||
|
|
||||
if (length.IsAbsolute) |
|
||||
return (MeasuredResult, 1); |
|
||||
if (length.IsAuto) |
|
||||
return (MeasuredResult, 2); |
|
||||
if (MinLength > 0) |
|
||||
return (MinLength, 3); |
|
||||
return (MeasuredResult, 4); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Visitor class used to gather the final length for a given SharedSizeGroup.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// The values are applied according to priorities defined in <see cref="MeasurementResult.GetPriorityLength"/>.
|
|
||||
/// </remarks>
|
|
||||
private class LentgthGatherer |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup
|
|
||||
/// </summary>
|
|
||||
public double Length { get; private set; } |
|
||||
private int gatheredPriority = 6; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Visits the <paramref name="result"/> applying the result of <see cref="MeasurementResult.GetPriorityLength"/> to its internal cache.
|
|
||||
/// </summary>
|
|
||||
/// <param name="result">The <see cref="MeasurementResult"/> instance to visit.</param>
|
|
||||
public void Visit(MeasurementResult result) |
|
||||
{ |
|
||||
var (length, priority) = result.GetPriorityLength(); |
|
||||
|
|
||||
if (gatheredPriority < priority) |
|
||||
return; |
|
||||
|
|
||||
gatheredPriority = priority; |
|
||||
if (gatheredPriority == priority) |
|
||||
{ |
|
||||
Length = Math.Max(length,Length); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Length = length; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value.
|
|
||||
/// </summary>
|
|
||||
private class Group |
|
||||
{ |
|
||||
private double? cachedResult; |
|
||||
private List<MeasurementResult> _results = new List<MeasurementResult>(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the name of the SharedSizeGroup.
|
|
||||
/// </summary>
|
|
||||
public string Name { get; } |
|
||||
|
|
||||
public Group(string name) |
|
||||
{ |
|
||||
Name = name; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the collection of the <see cref="MeasurementResult"/> instances.
|
|
||||
/// </summary>
|
|
||||
public IReadOnlyList<MeasurementResult> Results => _results; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup.
|
|
||||
/// </summary>
|
|
||||
public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clears the previously cached result in preparation for measurement.
|
|
||||
/// </summary>
|
|
||||
public void Reset() |
|
||||
{ |
|
||||
cachedResult = null; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Ads a measurement result to this group and sets it's <see cref="MeasurementResult.SizeGroup"/> property
|
|
||||
/// to this instance.
|
|
||||
/// </summary>
|
|
||||
/// <param name="result">The <see cref="MeasurementResult"/> to include in this group.</param>
|
|
||||
public void Add(MeasurementResult result) |
|
||||
{ |
|
||||
if (_results.Contains(result)) |
|
||||
throw new AvaloniaInternalException( |
|
||||
$"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); |
|
||||
|
|
||||
result.SizeGroup = this; |
|
||||
_results.Add(result); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes the measurement result from this group and clears its <see cref="MeasurementResult.SizeGroup"/> value.
|
|
||||
/// </summary>
|
|
||||
/// <param name="result">The <see cref="MeasurementResult"/> to clear.</param>
|
|
||||
public void Remove(MeasurementResult result) |
|
||||
{ |
|
||||
if (!_results.Contains(result)) |
|
||||
throw new AvaloniaInternalException( |
|
||||
$"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); |
|
||||
result.SizeGroup = null; |
|
||||
_results.Remove(result); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
private double Gather() |
|
||||
{ |
|
||||
var visitor = new LentgthGatherer(); |
|
||||
|
|
||||
_results.ForEach(visitor.Visit); |
|
||||
|
|
||||
return visitor.Length; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private readonly AvaloniaList<MeasurementCache> _measurementCaches = new AvaloniaList<MeasurementCache>(); |
|
||||
private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>(); |
|
||||
private bool _invalidating; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes the SharedSizeScope and notifies all affected grids of the change.
|
|
||||
/// </summary>
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
// while (_measurementCaches.Any())
|
|
||||
// _measurementCaches[0].Grid.SharedScopeChanged();
|
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree.
|
|
||||
/// </summary>
|
|
||||
/// <param name="toAdd">The <see cref="Grid"/> to add to this scope.</param>
|
|
||||
internal void RegisterGrid(Grid toAdd) |
|
||||
{ |
|
||||
if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) |
|
||||
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); |
|
||||
|
|
||||
var cache = new MeasurementCache(toAdd); |
|
||||
_measurementCaches.Add(cache); |
|
||||
AddGridToScopes(cache); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes the registration for a grid in this SharedSizeScope.
|
|
||||
/// </summary>
|
|
||||
/// <param name="toRemove">The <see cref="Grid"/> to remove.</param>
|
|
||||
internal void UnegisterGrid(Grid toRemove) |
|
||||
{ |
|
||||
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); |
|
||||
if (cache == null) |
|
||||
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); |
|
||||
|
|
||||
_measurementCaches.Remove(cache); |
|
||||
RemoveGridFromScopes(cache); |
|
||||
cache.Dispose(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope.
|
|
||||
/// </summary>
|
|
||||
/// <param name="toCheck">The <see cref="Grid"/> that should be checked.</param>
|
|
||||
/// <returns>True if the grid should forward its calls.</returns>
|
|
||||
internal bool ParticipatesInScope(Grid toCheck) |
|
||||
{ |
|
||||
return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) |
|
||||
?.Results.Any(r => r.SizeGroup != null) ?? false; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated.
|
|
||||
/// Forwards the same call to all affected grids in this scope.
|
|
||||
/// </summary>
|
|
||||
/// <param name="grid">The <see cref="Grid"/> that had it's Measure invalidated.</param>
|
|
||||
internal void InvalidateMeasure(Grid grid) |
|
||||
{ |
|
||||
// prevent stack overflow
|
|
||||
if (_invalidating) |
|
||||
return; |
|
||||
_invalidating = true; |
|
||||
|
|
||||
InvalidateMeasureImpl(grid); |
|
||||
|
|
||||
_invalidating = false; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Updates the measurement cache with the results of the <paramref name="grid"/> measurement pass.
|
|
||||
/// </summary>
|
|
||||
/// <param name="grid">The <see cref="Grid"/> that has been measured.</param>
|
|
||||
/// <param name="rowResult">Measurement result for the grid's <see cref="RowDefinitions"/></param>
|
|
||||
/// <param name="columnResult">Measurement result for the grid's <see cref="ColumnDefinitions"/></param>
|
|
||||
internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
|
||||
{ |
|
||||
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); |
|
||||
if (cache == null) |
|
||||
throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); |
|
||||
|
|
||||
cache.UpdateMeasureResult(rowResult, columnResult); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid.
|
|
||||
/// </summary>
|
|
||||
/// <param name="grid">The <see cref="Grid"/> that is being Arranged</param>
|
|
||||
/// <param name="rowResult">The <paramref name="grid"/>'s cached measurement result.</param>
|
|
||||
/// <param name="columnResult">The <paramref name="grid"/>'s cached measurement result.</param>
|
|
||||
/// <returns>Row and column measurement result updated with the SharedSizeScope constraints.</returns>
|
|
||||
internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) |
|
||||
{ |
|
||||
return ( |
|
||||
Arrange(grid.RowDefinitions, rowResult), |
|
||||
Arrange(grid.ColumnDefinitions, columnResult) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Invalidates the measure of all grids affected by the SharedSizeGroups contained within.
|
|
||||
/// </summary>
|
|
||||
/// <param name="grid">The <see cref="Grid"/> that is being invalidated.</param>
|
|
||||
private void InvalidateMeasureImpl(Grid grid) |
|
||||
{ |
|
||||
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); |
|
||||
|
|
||||
if (cache == null) |
|
||||
throw new AvaloniaInternalException( |
|
||||
$"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); |
|
||||
|
|
||||
// already invalidated the cache, early out.
|
|
||||
if (cache.MeasurementState == MeasurementState.Invalidated) |
|
||||
return; |
|
||||
|
|
||||
// we won't calculate, so we should not invalidate.
|
|
||||
if (!ParticipatesInScope(grid)) |
|
||||
return; |
|
||||
|
|
||||
cache.InvalidateMeasure(); |
|
||||
|
|
||||
// maybe there is a condition to only call arrange on some of the calls?
|
|
||||
grid.InvalidateMeasure(); |
|
||||
|
|
||||
// find all the scopes within the invalidated grid
|
|
||||
var scopeNames = cache.Results |
|
||||
.Where(mr => mr.SizeGroup != null) |
|
||||
.Select(mr => mr.SizeGroup.Name) |
|
||||
.Distinct(); |
|
||||
// find all grids related to those scopes
|
|
||||
var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) |
|
||||
.Select(r => r.OwningGrid) |
|
||||
.Where(g => g.IsMeasureValid) |
|
||||
.Distinct(); |
|
||||
|
|
||||
// invalidate them as well
|
|
||||
foreach (var otherGrid in otherGrids) |
|
||||
{ |
|
||||
InvalidateMeasureImpl(otherGrid); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// <see cref="IObserver{T}"/> callback notifying the scope that a <see cref="MeasurementResult"/> has changed its
|
|
||||
/// SharedSizeGroup
|
|
||||
/// </summary>
|
|
||||
/// <param name="change">Old and New name (either can be null) of the SharedSizeGroup, as well as the result.</param>
|
|
||||
private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) |
|
||||
{ |
|
||||
RemoveFromGroup(change.oldName, change.result); |
|
||||
AddToGroup(change.newName, change.result); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Handles the impact of SharedSizeGroups on the Arrange of <see cref="RowDefinitions"/>/<see cref="ColumnDefinitions"/>
|
|
||||
/// </summary>
|
|
||||
/// <param name="definitions">Rows/Columns that were measured</param>
|
|
||||
/// <param name="measureResult">The initial measurement result.</param>
|
|
||||
/// <returns>Modified measure result</returns>
|
|
||||
private GridLayout.MeasureResult Arrange(IReadOnlyList<DefinitionBase> definitions, GridLayout.MeasureResult measureResult) |
|
||||
{ |
|
||||
var conventions = measureResult.LeanLengthList.ToList(); |
|
||||
var lengths = measureResult.LengthList.ToList(); |
|
||||
var desiredLength = 0.0; |
|
||||
for (int i = 0; i < definitions.Count; i++) |
|
||||
{ |
|
||||
var definition = definitions[i]; |
|
||||
|
|
||||
// for empty SharedSizeGroups pass on unmodified result.
|
|
||||
if (string.IsNullOrEmpty(definition.SharedSizeGroup)) |
|
||||
{ |
|
||||
desiredLength += measureResult.LengthList[i]; |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
var group = _groups[definition.SharedSizeGroup]; |
|
||||
// Length calculated over all Definitions participating in a SharedSizeGroup.
|
|
||||
var length = group.CalculatedLength; |
|
||||
|
|
||||
conventions[i] = new GridLayout.LengthConvention( |
|
||||
new GridLength(length), |
|
||||
measureResult.LeanLengthList[i].MinLength, |
|
||||
measureResult.LeanLengthList[i].MaxLength |
|
||||
); |
|
||||
lengths[i] = length; |
|
||||
desiredLength += length; |
|
||||
} |
|
||||
|
|
||||
return new GridLayout.MeasureResult( |
|
||||
measureResult.ContainerLength, |
|
||||
desiredLength, |
|
||||
measureResult.GreedyDesiredLength,//??
|
|
||||
conventions, |
|
||||
lengths, |
|
||||
measureResult.MinLengths); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Adds all measurement results for a grid to their repsective scopes.
|
|
||||
/// </summary>
|
|
||||
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be added.</param>
|
|
||||
private void AddGridToScopes(MeasurementCache cache) |
|
||||
{ |
|
||||
cache.GroupChanged.Subscribe(SharedGroupChanged); |
|
||||
|
|
||||
foreach (var result in cache.Results) |
|
||||
{ |
|
||||
var scopeName = result.Definition.SharedSizeGroup; |
|
||||
AddToGroup(scopeName, result); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Handles adding the <see cref="MeasurementResult"/> to a SharedSizeGroup.
|
|
||||
/// Does nothing for empty SharedSizeGroups.
|
|
||||
/// </summary>
|
|
||||
/// <param name="scopeName">The name (can be null or empty) of the group to add the <paramref name="result"/> to.</param>
|
|
||||
/// <param name="result">The <see cref="MeasurementResult"/> to add to a scope.</param>
|
|
||||
private void AddToGroup(string scopeName, MeasurementResult result) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(scopeName)) |
|
||||
return; |
|
||||
|
|
||||
if (!_groups.TryGetValue(scopeName, out var group)) |
|
||||
_groups.Add(scopeName, group = new Group(scopeName)); |
|
||||
|
|
||||
group.Add(result); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes all measurement results for a grid from their respective scopes.
|
|
||||
/// </summary>
|
|
||||
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be removed.</param>
|
|
||||
private void RemoveGridFromScopes(MeasurementCache cache) |
|
||||
{ |
|
||||
foreach (var result in cache.Results) |
|
||||
{ |
|
||||
var scopeName = result.Definition.SharedSizeGroup; |
|
||||
RemoveFromGroup(scopeName, result); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Handles removing the <see cref="MeasurementResult"/> from a SharedSizeGroup.
|
|
||||
/// Does nothing for empty SharedSizeGroups.
|
|
||||
/// </summary>
|
|
||||
/// <param name="scopeName">The name (can be null or empty) of the group to remove the <paramref name="result"/> from.</param>
|
|
||||
/// <param name="result">The <see cref="MeasurementResult"/> to remove from a scope.</param>
|
|
||||
private void RemoveFromGroup(string scopeName, MeasurementResult result) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(scopeName)) |
|
||||
return; |
|
||||
|
|
||||
if (!_groups.TryGetValue(scopeName, out var group)) |
|
||||
throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); |
|
||||
|
|
||||
group.Remove(result); |
|
||||
if (!group.Results.Any()) |
|
||||
_groups.Remove(scopeName); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,184 +0,0 @@ |
|||||
using System.Collections; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.Linq; |
|
||||
using Avalonia.Controls.Utils; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Avalonia.Controls.UnitTests |
|
||||
{ |
|
||||
public class GridLayoutTests |
|
||||
{ |
|
||||
private const double Inf = double.PositiveInfinity; |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] |
|
||||
[InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] |
|
||||
[InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] |
|
||||
public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] |
|
||||
public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] |
|
||||
[InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] |
|
||||
[InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] |
|
||||
public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] |
|
||||
[InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] |
|
||||
[InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] |
|
||||
[InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] |
|
||||
[InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] |
|
||||
public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] |
|
||||
public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] |
|
||||
[InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] |
|
||||
[InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] |
|
||||
public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// This is needed because Mono somehow converts double array to object array in attribute metadata
|
|
||||
/// </summary>
|
|
||||
static void AssertEqual(IList expected, IReadOnlyList<double> actual) |
|
||||
{ |
|
||||
var conv = expected.Cast<double>().ToArray(); |
|
||||
Assert.Equal(conv, actual); |
|
||||
} |
|
||||
|
|
||||
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] |
|
||||
private static void TestRowDefinitionsOnly(string length, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
// Arrange
|
|
||||
var layout = new GridLayout(new RowDefinitions(length)); |
|
||||
|
|
||||
// Measure - Action & Assert
|
|
||||
var measure = layout.Measure(containerLength); |
|
||||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|
||||
AssertEqual(expectedLengthList, measure.LengthList); |
|
||||
|
|
||||
// Arrange - Action & Assert
|
|
||||
var arrange = layout.Arrange(containerLength, measure); |
|
||||
AssertEqual(expectedLengthList, arrange.LengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] |
|
||||
[InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] |
|
||||
[InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] |
|
||||
[InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] |
|
||||
[InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] |
|
||||
public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, |
|
||||
IList expectedMeasureList, IList expectedArrangeList) |
|
||||
{ |
|
||||
// Arrange
|
|
||||
var layout = new GridLayout(new RowDefinitions(length)); |
|
||||
|
|
||||
// Measure - Action & Assert
|
|
||||
var measure = layout.Measure(Inf); |
|
||||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|
||||
AssertEqual(expectedMeasureList, measure.LengthList); |
|
||||
|
|
||||
// Arrange - Action & Assert
|
|
||||
var arrange = layout.Arrange(measure.DesiredLength, measure); |
|
||||
AssertEqual(expectedArrangeList, arrange.LengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] |
|
||||
public void MeasureArrange_ChildHasSize_Correct(string length, |
|
||||
IList childLengthList, double containerLength, |
|
||||
double expectedDesiredLength, IList expectedLengthList) |
|
||||
{ |
|
||||
// Arrange
|
|
||||
var lengthList = new ColumnDefinitions(length); |
|
||||
var layout = new GridLayout(lengthList); |
|
||||
layout.AppendMeasureConventions( |
|
||||
Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), |
|
||||
x => (double)childLengthList[x]); |
|
||||
|
|
||||
// Measure - Action & Assert
|
|
||||
var measure = layout.Measure(containerLength); |
|
||||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|
||||
AssertEqual(expectedLengthList, measure.LengthList); |
|
||||
|
|
||||
// Arrange - Action & Assert
|
|
||||
var arrange = layout.Arrange(containerLength, measure); |
|
||||
AssertEqual(expectedLengthList, arrange.LengthList); |
|
||||
} |
|
||||
|
|
||||
[Theory] |
|
||||
[InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] |
|
||||
[InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] |
|
||||
[InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] |
|
||||
[InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] |
|
||||
[InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] |
|
||||
public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( |
|
||||
double containerLength, double expectedDesiredLength, |
|
||||
IList expectedMeasureLengthList, IList expectedArrangeLengthList) |
|
||||
{ |
|
||||
var length = "100,*,2*"; |
|
||||
var childLengthList = new[] { 150d, 150d, 150d }; |
|
||||
var spans = new[] { 1, 2, 1 }; |
|
||||
|
|
||||
// Arrange
|
|
||||
var lengthList = new ColumnDefinitions(length); |
|
||||
var layout = new GridLayout(lengthList); |
|
||||
layout.AppendMeasureConventions( |
|
||||
Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), |
|
||||
x => childLengthList[x]); |
|
||||
|
|
||||
// Measure - Action & Assert
|
|
||||
var measure = layout.Measure(containerLength); |
|
||||
Assert.Equal(expectedDesiredLength, measure.DesiredLength); |
|
||||
AssertEqual(expectedMeasureLengthList, measure.LengthList); |
|
||||
|
|
||||
// Arrange - Action & Assert
|
|
||||
var arrange = layout.Arrange( |
|
||||
double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, |
|
||||
measure); |
|
||||
AssertEqual(expectedArrangeLengthList, arrange.LengthList); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue