Browse Source

Merge pull request #1517 from dotnet-campus/t/lvyi/fix-1516

A new grid layout algorithm to improve performance and fix some bugs
danwalmsley 7 years ago
parent
commit
0e1d494ba6

+ 1 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>latest</LangVersion>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

+ 148 - 674
src/Avalonia.Controls/Grid.cs

@@ -4,7 +4,10 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Runtime.CompilerServices;
 using Avalonia.Collections;
+using Avalonia.Controls.Utils;
+using JetBrains.Annotations;
 
 namespace Avalonia.Controls
 {
@@ -45,10 +48,6 @@ namespace Avalonia.Controls
 
         private RowDefinitions _rowDefinitions;
 
-        private Segment[,] _rowMatrix;
-
-        private Segment[,] _colMatrix;
-
         /// <summary>
         /// Gets or sets the columns definitions for the grid.
         /// </summary>
@@ -183,6 +182,18 @@ namespace Avalonia.Controls
             element.SetValue(RowSpanProperty, value);
         }
 
+        /// <summary>
+        /// Gets the result of the last column measurement.
+        /// Use this result to reduce the arrange calculation.
+        /// </summary>
+        private GridLayout.MeasureResult _columnMeasureCache;
+
+        /// <summary>
+        /// Gets the result of the last row measurement.
+        /// Use this result to reduce the arrange calculation.
+        /// </summary>
+        private GridLayout.MeasureResult _rowMeasureCache;
+
         /// <summary>
         /// Measures the grid.
         /// </summary>
@@ -190,293 +201,74 @@ namespace Avalonia.Controls
         /// <returns>The desired size of the control.</returns>
         protected override Size MeasureOverride(Size constraint)
         {
-            Size totalSize = constraint;
-            int colCount = ColumnDefinitions.Count;
-            int rowCount = RowDefinitions.Count;
-            double totalStarsX = 0;
-            double totalStarsY = 0;
-            bool emptyRows = rowCount == 0;
-            bool emptyCols = colCount == 0;
-            bool hasChildren = Children.Count > 0;
-
-            if (emptyRows)
-            {
-                rowCount = 1;
-            }
-
-            if (emptyCols)
-            {
-                colCount = 1;
-            }
-
-            CreateMatrices(rowCount, colCount);
+            // Situation 1/2:
+            // If the grid doesn't have any column/row definitions, it behaves like a normal panel.
+            // GridLayout supports this situation but we handle this separately for performance.
 
-            if (emptyRows)
-            {
-                _rowMatrix[0, 0] = new Segment(0, 0, double.PositiveInfinity, GridUnitType.Star);
-                _rowMatrix[0, 0].Stars = 1.0;
-                totalStarsY += 1.0;
-            }
-            else
+            if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0)
             {
-                for (int i = 0; i < rowCount; i++)
+                var maxWidth = 0.0;
+                var maxHeight = 0.0;
+                foreach (var child in Children.OfType<Control>())
                 {
-                    RowDefinition rowdef = RowDefinitions[i];
-                    GridLength height = rowdef.Height;
-
-                    rowdef.ActualHeight = double.PositiveInfinity;
-                    _rowMatrix[i, i] = new Segment(0, rowdef.MinHeight, rowdef.MaxHeight, height.GridUnitType);
-
-                    if (height.GridUnitType == GridUnitType.Pixel)
-                    {
-                        _rowMatrix[i, i].OfferedSize = Clamp(height.Value, _rowMatrix[i, i].Min, _rowMatrix[i, i].Max);
-                        _rowMatrix[i, i].DesiredSize = _rowMatrix[i, i].OfferedSize;
-                        rowdef.ActualHeight = _rowMatrix[i, i].OfferedSize;
-                    }
-                    else if (height.GridUnitType == GridUnitType.Star)
-                    {
-                        _rowMatrix[i, i].Stars = height.Value;
-                        totalStarsY += height.Value;
-                    }
-                    else if (height.GridUnitType == GridUnitType.Auto)
-                    {
-                        _rowMatrix[i, i].OfferedSize = Clamp(0, _rowMatrix[i, i].Min, _rowMatrix[i, i].Max);
-                        _rowMatrix[i, i].DesiredSize = _rowMatrix[i, i].OfferedSize;
-                    }
+                    child.Measure(constraint);
+                    maxWidth = Math.Max(maxWidth, child.DesiredSize.Width);
+                    maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);
                 }
-            }
 
-            if (emptyCols)
-            {
-                _colMatrix[0, 0] = new Segment(0, 0, double.PositiveInfinity, GridUnitType.Star);
-                _colMatrix[0, 0].Stars = 1.0;
-                totalStarsX += 1.0;
-            }
-            else
-            {
-                for (int i = 0; i < colCount; i++)
-                {
-                    ColumnDefinition coldef = ColumnDefinitions[i];
-                    GridLength width = coldef.Width;
-
-                    coldef.ActualWidth = double.PositiveInfinity;
-                    _colMatrix[i, i] = new Segment(0, coldef.MinWidth, coldef.MaxWidth, width.GridUnitType);
-
-                    if (width.GridUnitType == GridUnitType.Pixel)
-                    {
-                        _colMatrix[i, i].OfferedSize = Clamp(width.Value, _colMatrix[i, i].Min, _colMatrix[i, i].Max);
-                        _colMatrix[i, i].DesiredSize = _colMatrix[i, i].OfferedSize;
-                        coldef.ActualWidth = _colMatrix[i, i].OfferedSize;
-                    }
-                    else if (width.GridUnitType == GridUnitType.Star)
-                    {
-                        _colMatrix[i, i].Stars = width.Value;
-                        totalStarsX += width.Value;
-                    }
-                    else if (width.GridUnitType == GridUnitType.Auto)
-                    {
-                        _colMatrix[i, i].OfferedSize = Clamp(0, _colMatrix[i, i].Min, _colMatrix[i, i].Max);
-                        _colMatrix[i, i].DesiredSize = _colMatrix[i, i].OfferedSize;
-                    }
-                }
+                maxWidth = Math.Min(maxWidth, constraint.Width);
+                maxHeight = Math.Min(maxHeight, constraint.Height);
+                return new Size(maxWidth, maxHeight);
             }
 
-            List<GridNode> sizes = new List<GridNode>();
-            GridNode node;
-            GridNode separator = new GridNode(null, 0, 0, 0);
-            int separatorIndex;
+            // Situation 2/2:
+            // If the grid defines some columns or rows.
+            // Debug Tip:
+            //     - GridLayout doesn't hold any state, so you can drag the debugger execution
+            //       arrow back to any statements and re-run them without any side-effect.
 
-            sizes.Add(separator);
+            var measureCache = new Dictionary<Control, Size>();
+            var (safeColumns, safeRows) = GetSafeColumnRows();
+            var columnLayout = new GridLayout(ColumnDefinitions);
+            var rowLayout = new GridLayout(RowDefinitions);
+            // Note: If a child stays in a * or Auto column/row, use constraint to measure it.
+            columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width);
+            rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height);
 
-            // Pre-process the grid children so that we know what types of elements we have so
-            // we can apply our special measuring rules.
-            GridWalker gridWalker = new GridWalker(this, _rowMatrix, _colMatrix);
+            // Calculate measurement.
+            var columnResult = columnLayout.Measure(constraint.Width);
+            var rowResult = rowLayout.Measure(constraint.Height);
 
-            for (int i = 0; i < 6; i++)
+            // Use the results of the measurement to measure the rest of the children.
+            foreach (var child in Children.OfType<Control>())
             {
-                // These bools tell us which grid element type we should be measuring. i.e.
-                // 'star/auto' means we should measure elements with a star row and auto col
-                bool autoAuto = i == 0;
-                bool starAuto = i == 1;
-                bool autoStar = i == 2;
-                bool starAutoAgain = i == 3;
-                bool nonStar = i == 4;
-                bool remainingStar = i == 5;
-
-                if (hasChildren)
-                {
-                    ExpandStarCols(totalSize);
-                    ExpandStarRows(totalSize);
-                }
+                var (column, columnSpan) = safeColumns[child];
+                var (row, rowSpan) = safeRows[child];
+                var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum();
+                var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum();
 
-                foreach (Control child in Children)
-                {
-                    int col, row;
-                    int colspan, rowspan;
-                    double childSizeX = 0;
-                    double childSizeY = 0;
-                    bool starCol = false;
-                    bool starRow = false;
-                    bool autoCol = false;
-                    bool autoRow = false;
-
-                    col = Math.Min(GetColumn(child), colCount - 1);
-                    row = Math.Min(GetRow(child), rowCount - 1);
-                    colspan = Math.Min(GetColumnSpan(child), colCount - col);
-                    rowspan = Math.Min(GetRowSpan(child), rowCount - row);
-
-                    for (int r = row; r < row + rowspan; r++)
-                    {
-                        starRow |= _rowMatrix[r, r].Type == GridUnitType.Star;
-                        autoRow |= _rowMatrix[r, r].Type == GridUnitType.Auto;
-                    }
-
-                    for (int c = col; c < col + colspan; c++)
-                    {
-                        starCol |= _colMatrix[c, c].Type == GridUnitType.Star;
-                        autoCol |= _colMatrix[c, c].Type == GridUnitType.Auto;
-                    }
-
-                    // This series of if statements checks whether or not we should measure
-                    // the current element and also if we need to override the sizes
-                    // passed to the Measure call.
-
-                    // If the element has Auto rows and Auto columns and does not span Star
-                    // rows/cols it should only be measured in the auto_auto phase.
-                    // There are similar rules governing auto/star and star/auto elements.
-                    // NOTE: star/auto elements are measured twice. The first time with
-                    // an override for height, the second time without it.
-                    if (autoRow && autoCol && !starRow && !starCol)
-                    {
-                        if (!autoAuto)
-                        {
-                            continue;
-                        }
-
-                        childSizeX = double.PositiveInfinity;
-                        childSizeY = double.PositiveInfinity;
-                    }
-                    else if (starRow && autoCol && !starCol)
-                    {
-                        if (!(starAuto || starAutoAgain))
-                        {
-                            continue;
-                        }
-
-                        if (starAuto && gridWalker.HasAutoStar)
-                        {
-                            childSizeY = double.PositiveInfinity;
-                        }
-
-                        childSizeX = double.PositiveInfinity;
-                    }
-                    else if (autoRow && starCol && !starRow)
-                    {
-                        if (!autoStar)
-                        {
-                            continue;
-                        }
-
-                        childSizeY = double.PositiveInfinity;
-                    }
-                    else if ((autoRow || autoCol) && !(starRow || starCol))
-                    {
-                        if (!nonStar)
-                        {
-                            continue;
-                        }
-
-                        if (autoRow)
-                        {
-                            childSizeY = double.PositiveInfinity;
-                        }
-
-                        if (autoCol)
-                        {
-                            childSizeX = double.PositiveInfinity;
-                        }
-                    }
-                    else if (!(starRow || starCol))
-                    {
-                        if (!nonStar)
-                        {
-                            continue;
-                        }
-                    }
-                    else
-                    {
-                        if (!remainingStar)
-                        {
-                            continue;
-                        }
-                    }
-
-                    for (int r = row; r < row + rowspan; r++)
-                    {
-                        childSizeY += _rowMatrix[r, r].OfferedSize;
-                    }
-
-                    for (int c = col; c < col + colspan; c++)
-                    {
-                        childSizeX += _colMatrix[c, c].OfferedSize;
-                    }
-
-                    child.Measure(new Size(childSizeX, childSizeY));
-                    Size desired = child.DesiredSize;
-
-                    // Elements distribute their height based on two rules:
-                    // 1) Elements with rowspan/colspan == 1 distribute their height first
-                    // 2) Everything else distributes in a LIFO manner.
-                    // As such, add all UIElements with rowspan/colspan == 1 after the separator in
-                    // the list and everything else before it. Then to process, just keep popping
-                    // elements off the end of the list.
-                    if (!starAuto)
-                    {
-                        node = new GridNode(_rowMatrix, row + rowspan - 1, row, desired.Height);
-                        separatorIndex = sizes.IndexOf(separator);
-                        sizes.Insert(node.Row == node.Column ? separatorIndex + 1 : separatorIndex, node);
-                    }
-
-                    node = new GridNode(_colMatrix, col + colspan - 1, col, desired.Width);
-
-                    separatorIndex = sizes.IndexOf(separator);
-                    sizes.Insert(node.Row == node.Column ? separatorIndex + 1 : separatorIndex, node);
-                }
+                MeasureOnce(child, new Size(width, height));
+            }
 
-                sizes.Remove(separator);
+            // Cache the measure result and return the desired size.
+            _columnMeasureCache = columnResult;
+            _rowMeasureCache = rowResult;
+            return new Size(columnResult.DesiredLength, rowResult.DesiredLength);
 
-                while (sizes.Count > 0)
+            // Measure each child only once.
+            // If a child has been measured, it will just return the desired size.
+            Size MeasureOnce(Control child, Size size)
+            {
+                if (measureCache.TryGetValue(child, out var desiredSize))
                 {
-                    node = sizes.Last();
-                    node.Matrix[node.Row, node.Column].DesiredSize = Math.Max(node.Matrix[node.Row, node.Column].DesiredSize, node.Size);
-                    AllocateDesiredSize(rowCount, colCount);
-                    sizes.Remove(node);
+                    return desiredSize;
                 }
 
-                sizes.Add(separator);
-            }
-
-            // Once we have measured and distributed all sizes, we have to store
-            // the results. Every time we want to expand the rows/cols, this will
-            // be used as the baseline.
-            SaveMeasureResults();
-
-            sizes.Remove(separator);
-
-            double gridSizeX = 0;
-            double gridSizeY = 0;
-
-            for (int c = 0; c < colCount; c++)
-            {
-                gridSizeX += _colMatrix[c, c].DesiredSize;
+                child.Measure(size);
+                desiredSize = child.DesiredSize;
+                measureCache[child] = desiredSize;
+                return desiredSize;
             }
-
-            for (int r = 0; r < rowCount; r++)
-            {
-                gridSizeY += _rowMatrix[r, r].DesiredSize;
-            }
-
-            return new Size(gridSizeX, gridSizeY);
         }
 
         /// <summary>
@@ -486,456 +278,138 @@ namespace Avalonia.Controls
         /// <returns>The space taken.</returns>
         protected override Size ArrangeOverride(Size finalSize)
         {
-            int colCount = ColumnDefinitions.Count;
-            int rowCount = RowDefinitions.Count;
-            int colMatrixDim = _colMatrix.GetLength(0);
-            int rowMatrixDim = _rowMatrix.GetLength(0);
-
-            RestoreMeasureResults();
+            // Situation 1/2:
+            // If the grid doesn't have any column/row definitions, it behaves like a normal panel.
+            // GridLayout supports this situation but we handle this separately for performance.
 
-            double totalConsumedX = 0;
-            double totalConsumedY = 0;
-
-            for (int c = 0; c < colMatrixDim; c++)
+            if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0)
             {
-                _colMatrix[c, c].OfferedSize = _colMatrix[c, c].DesiredSize;
-                totalConsumedX += _colMatrix[c, c].OfferedSize;
-            }
-
-            for (int r = 0; r < rowMatrixDim; r++)
-            {
-                _rowMatrix[r, r].OfferedSize = _rowMatrix[r, r].DesiredSize;
-                totalConsumedY += _rowMatrix[r, r].OfferedSize;
-            }
-
-            if (totalConsumedX != finalSize.Width)
-            {
-                ExpandStarCols(finalSize);
-            }
-
-            if (totalConsumedY != finalSize.Height)
-            {
-                ExpandStarRows(finalSize);
-            }
-
-            for (int c = 0; c < colCount; c++)
-            {
-                ColumnDefinitions[c].ActualWidth = _colMatrix[c, c].OfferedSize;
-            }
-
-            for (int r = 0; r < rowCount; r++)
-            {
-                RowDefinitions[r].ActualHeight = _rowMatrix[r, r].OfferedSize;
-            }
-
-            foreach (Control child in Children)
-            {
-                int col = Math.Min(GetColumn(child), colMatrixDim - 1);
-                int row = Math.Min(GetRow(child), rowMatrixDim - 1);
-                int colspan = Math.Min(GetColumnSpan(child), colMatrixDim - col);
-                int rowspan = Math.Min(GetRowSpan(child), rowMatrixDim - row);
-
-                double childFinalX = 0;
-                double childFinalY = 0;
-                double childFinalW = 0;
-                double childFinalH = 0;
-
-                for (int c = 0; c < col; c++)
-                {
-                    childFinalX += _colMatrix[c, c].OfferedSize;
-                }
-
-                for (int c = col; c < col + colspan; c++)
+                foreach (var child in Children.OfType<Control>())
                 {
-                    childFinalW += _colMatrix[c, c].OfferedSize;
+                    child.Arrange(new Rect(finalSize));
                 }
 
-                for (int r = 0; r < row; r++)
-                {
-                    childFinalY += _rowMatrix[r, r].OfferedSize;
-                }
-
-                for (int r = row; r < row + rowspan; r++)
-                {
-                    childFinalH += _rowMatrix[r, r].OfferedSize;
-                }
-
-                child.Arrange(new Rect(childFinalX, childFinalY, childFinalW, childFinalH));
+                return finalSize;
             }
 
-            return finalSize;
-        }
-
-        private static double Clamp(double val, double min, double max)
-        {
-            if (val < min)
-            {
-                return min;
-            }
-            else if (val > max)
-            {
-                return max;
-            }
-            else
-            {
-                return val;
-            }
-        }
+            // Situation 2/2:
+            // If the grid defines some columns or rows.
+            // Debug Tip:
+            //     - GridLayout doesn't hold any state, so you can drag the debugger execution
+            //       arrow back to any statements and re-run them without any side-effect.
 
-        private static int ValidateColumn(AvaloniaObject o, int value)
-        {
-            if (value < 0)
-            {
-                throw new ArgumentException("Invalid Grid.Column value.");
-            }
+            var (safeColumns, safeRows) = GetSafeColumnRows();
+            var columnLayout = new GridLayout(ColumnDefinitions);
+            var rowLayout = new GridLayout(RowDefinitions);
 
-            return value;
-        }
+            // Calculate for arrange result.
+            var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache);
+            var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache);
 
-        private static int ValidateRow(AvaloniaObject o, int value)
-        {
-            if (value < 0)
+            // Arrange the children.
+            foreach (var child in Children.OfType<Control>())
             {
-                throw new ArgumentException("Invalid Grid.Row value.");
-            }
+                var (column, columnSpan) = safeColumns[child];
+                var (row, rowSpan) = safeRows[child];
+                var x = Enumerable.Range(0, column).Sum(c => columnResult.LengthList[c]);
+                var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]);
+                var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]);
+                var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]);
 
-            return value;
-        }
-
-        private void CreateMatrices(int rowCount, int colCount)
-        {
-            if (_rowMatrix == null || _colMatrix == null ||
-                _rowMatrix.GetLength(0) != rowCount ||
-                _colMatrix.GetLength(0) != colCount)
-            {
-                _rowMatrix = new Segment[rowCount, rowCount];
-                _colMatrix = new Segment[colCount, colCount];
-            }
-            else
-            {
-                Array.Clear(_rowMatrix, 0, _rowMatrix.Length);
-                Array.Clear(_colMatrix, 0, _colMatrix.Length);
+                child.Arrange(new Rect(x, y, width, height));
             }
-        }
-
-        private void ExpandStarCols(Size availableSize)
-        {
-            int matrixCount = _colMatrix.GetLength(0);
-            int columnsCount = ColumnDefinitions.Count;
-            double width = availableSize.Width;
 
-            for (int i = 0; i < matrixCount; i++)
+            // Assign the actual width.
+            for (var i = 0; i < ColumnDefinitions.Count; i++)
             {
-                if (_colMatrix[i, i].Type == GridUnitType.Star)
-                {
-                    _colMatrix[i, i].OfferedSize = 0;
-                }
-                else
-                {
-                    width = Math.Max(width - _colMatrix[i, i].OfferedSize, 0);
-                }
+                ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i];
             }
 
-            AssignSize(_colMatrix, 0, matrixCount - 1, ref width, GridUnitType.Star, false);
-            width = Math.Max(0, width);
-
-            if (columnsCount > 0)
+            // Assign the actual height.
+            for (var i = 0; i < RowDefinitions.Count; i++)
             {
-                for (int i = 0; i < matrixCount; i++)
-                {
-                    if (_colMatrix[i, i].Type == GridUnitType.Star)
-                    {
-                        ColumnDefinitions[i].ActualWidth = _colMatrix[i, i].OfferedSize;
-                    }
-                }
-            }
-        }
-
-        private void ExpandStarRows(Size availableSize)
-        {
-            int matrixCount = _rowMatrix.GetLength(0);
-            int rowCount = RowDefinitions.Count;
-            double height = availableSize.Height;
-
-            // When expanding star rows, we need to zero out their height before
-            // calling AssignSize. AssignSize takes care of distributing the
-            // available size when there are Mins and Maxs applied.
-            for (int i = 0; i < matrixCount; i++)
-            {
-                if (_rowMatrix[i, i].Type == GridUnitType.Star)
-                {
-                    _rowMatrix[i, i].OfferedSize = 0.0;
-                }
-                else
-                {
-                    height = Math.Max(height - _rowMatrix[i, i].OfferedSize, 0);
-                }
+                RowDefinitions[i].ActualHeight = rowResult.LengthList[i];
             }
 
-            AssignSize(_rowMatrix, 0, matrixCount - 1, ref height, GridUnitType.Star, false);
-
-            if (rowCount > 0)
-            {
-                for (int i = 0; i < matrixCount; i++)
-                {
-                    if (_rowMatrix[i, i].Type == GridUnitType.Star)
-                    {
-                        RowDefinitions[i].ActualHeight = _rowMatrix[i, i].OfferedSize;
-                    }
-                }
-            }
+            // Return the render size.
+            return finalSize;
         }
 
-        private void AssignSize(
-            Segment[,] matrix,
-            int start,
-            int end,
-            ref double size,
-            GridUnitType type,
-            bool desiredSize)
+        /// <summary>
+        /// Get the safe column/columnspan and safe row/rowspan.
+        /// This method ensures that none of the children has a column/row outside the bounds of the definitions.
+        /// </summary>
+        [Pure]
+        private (Dictionary<Control, (int index, int span)> safeColumns,
+            Dictionary<Control, (int index, int span)> safeRows) GetSafeColumnRows()
         {
-            double count = 0;
-            bool assigned;
-
-            // Count how many segments are of the correct type. If we're measuring Star rows/cols
-            // we need to count the number of stars instead.
-            for (int i = start; i <= end; i++)
-            {
-                double segmentSize = desiredSize ? matrix[i, i].DesiredSize : matrix[i, i].OfferedSize;
-                if (segmentSize < matrix[i, i].Max)
-                {
-                    count += type == GridUnitType.Star ? matrix[i, i].Stars : 1;
-                }
-            }
-
-            do
-            {
-                double contribution = size / count;
-
-                assigned = false;
-
-                for (int i = start; i <= end; i++)
-                {
-                    double segmentSize = desiredSize ? matrix[i, i].DesiredSize : matrix[i, i].OfferedSize;
-
-                    if (!(matrix[i, i].Type == type && segmentSize < matrix[i, i].Max))
-                    {
-                        continue;
-                    }
-
-                    double newsize = segmentSize;
-                    newsize += contribution * (type == GridUnitType.Star ? matrix[i, i].Stars : 1);
-                    newsize = Math.Min(newsize, matrix[i, i].Max);
-                    assigned |= newsize > segmentSize;
-                    size -= newsize - segmentSize;
-
-                    if (desiredSize)
-                    {
-                        matrix[i, i].DesiredSize = newsize;
-                    }
-                    else
-                    {
-                        matrix[i, i].OfferedSize = newsize;
-                    }
-                }
-            }
-            while (assigned);
+            var columnCount = ColumnDefinitions.Count;
+            var rowCount = RowDefinitions.Count;
+            columnCount = columnCount == 0 ? 1 : columnCount;
+            rowCount = rowCount == 0 ? 1 : rowCount;
+            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);
         }
 
-        private void AllocateDesiredSize(int rowCount, int colCount)
+        /// <summary>
+        /// Gets the safe row/column and rowspan/columnspan for a specified range.
+        /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside.
+        /// </summary>
+        /// <param name="length">The row or column count.</param>
+        /// <param name="userIndex">The row or column that the user assigned.</param>
+        /// <param name="userSpan">The rowspan or columnspan that the user assigned.</param>
+        /// <returns>The safe row/column and rowspan/columnspan.</returns>
+        [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan)
         {
-            // First allocate the heights of the RowDefinitions, then allocate
-            // the widths of the ColumnDefinitions.
-            for (int i = 0; i < 2; i++)
-            {
-                Segment[,] matrix = i == 0 ? _rowMatrix : _colMatrix;
-                int count = i == 0 ? rowCount : colCount;
-
-                for (int row = count - 1; row >= 0; row--)
-                {
-                    for (int col = row; col >= 0; col--)
-                    {
-                        bool spansStar = false;
-                        for (int j = row; j >= col; j--)
-                        {
-                            spansStar |= matrix[j, j].Type == GridUnitType.Star;
-                        }
-
-                        // This is the amount of pixels which must be available between the grid rows
-                        // at index 'col' and 'row'. i.e. if 'row' == 0 and 'col' == 2, there must
-                        // be at least 'matrix [row][col].size' pixels of height allocated between
-                        // all the rows in the range col -> row.
-                        double current = matrix[row, col].DesiredSize;
-
-                        // Count how many pixels have already been allocated between the grid rows
-                        // in the range col -> row. The amount of pixels allocated to each grid row/column
-                        // is found on the diagonal of the matrix.
-                        double totalAllocated = 0;
-
-                        for (int k = row; k >= col; k--)
-                        {
-                            totalAllocated += matrix[k, k].DesiredSize;
-                        }
-
-                        // If the size requirement has not been met, allocate the additional required
-                        // size between 'pixel' rows, then 'star' rows, finally 'auto' rows, until all
-                        // height has been assigned.
-                        if (totalAllocated < current)
-                        {
-                            double additional = current - totalAllocated;
-
-                            if (spansStar)
-                            {
-                                AssignSize(matrix, col, row, ref additional, GridUnitType.Star, true);
-                            }
-                            else
-                            {
-                                AssignSize(matrix, col, row, ref additional, GridUnitType.Pixel, true);
-                                AssignSize(matrix, col, row, ref additional, GridUnitType.Auto, true);
-                            }
-                        }
-                    }
-                }
-            }
-
-            int rowMatrixDim = _rowMatrix.GetLength(0);
-            int colMatrixDim = _colMatrix.GetLength(0);
+            var index = userIndex;
+            var span = userSpan;
 
-            for (int r = 0; r < rowMatrixDim; r++)
+            if (index < 0)
             {
-                _rowMatrix[r, r].OfferedSize = _rowMatrix[r, r].DesiredSize;
+                span = index + span;
+                index = 0;
             }
 
-            for (int c = 0; c < colMatrixDim; c++)
+            if (span <= 0)
             {
-                _colMatrix[c, c].OfferedSize = _colMatrix[c, c].DesiredSize;
+                span = 1;
             }
-        }
 
-        private void SaveMeasureResults()
-        {
-            int rowMatrixDim = _rowMatrix.GetLength(0);
-            int colMatrixDim = _colMatrix.GetLength(0);
-
-            for (int i = 0; i < rowMatrixDim; i++)
+            if (userIndex >= length)
             {
-                for (int j = 0; j < rowMatrixDim; j++)
-                {
-                    _rowMatrix[i, j].OriginalSize = _rowMatrix[i, j].OfferedSize;
-                }
+                index = length - 1;
+                span = 1;
             }
-
-            for (int i = 0; i < colMatrixDim; i++)
+            else if (userIndex + userSpan > length)
             {
-                for (int j = 0; j < colMatrixDim; j++)
-                {
-                    _colMatrix[i, j].OriginalSize = _colMatrix[i, j].OfferedSize;
-                }
-            }
-        }
-
-        private void RestoreMeasureResults()
-        {
-            int rowMatrixDim = _rowMatrix.GetLength(0);
-            int colMatrixDim = _colMatrix.GetLength(0);
-
-            for (int i = 0; i < rowMatrixDim; i++)
-            {
-                for (int j = 0; j < rowMatrixDim; j++)
-                {
-                    _rowMatrix[i, j].OfferedSize = _rowMatrix[i, j].OriginalSize;
-                }
+                span = length - userIndex;
             }
 
-            for (int i = 0; i < colMatrixDim; i++)
-            {
-                for (int j = 0; j < colMatrixDim; j++)
-                {
-                    _colMatrix[i, j].OfferedSize = _colMatrix[i, j].OriginalSize;
-                }
-            }
+            return (index, span);
         }
 
-        private struct Segment
+        private static int ValidateColumn(AvaloniaObject o, int value)
         {
-            public double OriginalSize;
-            public double Max;
-            public double Min;
-            public double DesiredSize;
-            public double OfferedSize;
-            public double Stars;
-            public GridUnitType Type;
-
-            public Segment(double offeredSize, double min, double max, GridUnitType type)
+            if (value < 0)
             {
-                OriginalSize = 0;
-                Min = min;
-                Max = max;
-                DesiredSize = 0;
-                OfferedSize = offeredSize;
-                Stars = 0;
-                Type = type;
+                throw new ArgumentException("Invalid Grid.Column value.");
             }
-        }
 
-        private struct GridNode
-        {
-            public readonly int Row;
-            public readonly int Column;
-            public readonly double Size;
-            public readonly Segment[,] Matrix;
-
-            public GridNode(Segment[,] matrix, int row, int col, double size)
-            {
-                Matrix = matrix;
-                Row = row;
-                Column = col;
-                Size = size;
-            }
+            return value;
         }
 
-        private class GridWalker
+        private static int ValidateRow(AvaloniaObject o, int value)
         {
-            public GridWalker(Grid grid, Segment[,] rowMatrix, Segment[,] colMatrix)
+            if (value < 0)
             {
-                int rowMatrixDim = rowMatrix.GetLength(0);
-                int colMatrixDim = colMatrix.GetLength(0);
-
-                foreach (Control child in grid.Children)
-                {
-                    bool starCol = false;
-                    bool starRow = false;
-                    bool autoCol = false;
-                    bool autoRow = false;
-
-                    int col = Math.Min(GetColumn(child), colMatrixDim - 1);
-                    int row = Math.Min(GetRow(child), rowMatrixDim - 1);
-                    int colspan = Math.Min(GetColumnSpan(child), colMatrixDim - 1);
-                    int rowspan = Math.Min(GetRowSpan(child), rowMatrixDim - 1);
-
-                    for (int r = row; r < row + rowspan; r++)
-                    {
-                        starRow |= rowMatrix[r, r].Type == GridUnitType.Star;
-                        autoRow |= rowMatrix[r, r].Type == GridUnitType.Auto;
-                    }
-
-                    for (int c = col; c < col + colspan; c++)
-                    {
-                        starCol |= colMatrix[c, c].Type == GridUnitType.Star;
-                        autoCol |= colMatrix[c, c].Type == GridUnitType.Auto;
-                    }
-
-                    HasAutoAuto |= autoRow && autoCol && !starRow && !starCol;
-                    HasStarAuto |= starRow && autoCol;
-                    HasAutoStar |= autoRow && starCol;
-                }
+                throw new ArgumentException("Invalid Grid.Row value.");
             }
 
-            public bool HasAutoAuto { get; }
-
-            public bool HasStarAuto { get; }
-
-            public bool HasAutoStar { get; }
+            return value;
         }
     }
-}
+}

+ 700 - 0
src/Avalonia.Controls/Utils/GridLayout.cs

@@ -0,0 +1,700 @@
+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>
+        /// <returns>
+        /// The measured result that containing the desired size and all the column/row lengths.
+        /// </returns>
+        [NotNull, Pure]
+        internal MeasureResult Measure(double containerLength)
+        {
+            // Prepare all the variables that this method needs to use.
+            var conventions = _conventions.Select(x => x.Clone()).ToList();
+            var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value);
+            var aggregatedLength = 0.0;
+            double starUnitLength;
+
+            // M2/7. 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 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);
+        }
+
+        /// <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);
+            }
+            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);
+            }
+
+            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 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 maximun 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.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)
+            {
+                ContainerLength = containerLength;
+                DesiredLength = desiredLength;
+                GreedyDesiredLength = greedyDesiredLength;
+                LeanLengthList = leanConventions;
+                LengthList = expandedConventions;
+            }
+
+            /// <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; }
+        }
+
+        /// <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 - 1
src/Avalonia.Themes.Default/CalendarItem.xaml

@@ -27,7 +27,7 @@
                 </Grid.RowDefinitions>
                 <Grid.ColumnDefinitions>
                   <ColumnDefinition Width="Auto" />
-                  <ColumnDefinition Width="Auto" />
+                  <ColumnDefinition Width="*" />
                   <ColumnDefinition Width="Auto" />
                 </Grid.ColumnDefinitions>
 

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@@ -1,6 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
   <PropertyGroup>
     <TargetFrameworks>netcoreapp2.0</TargetFrameworks>
+    <LangVersion>latest</LangVersion>
     <OutputType>Library</OutputType>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />

+ 173 - 0
tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs

@@ -0,0 +1,173 @@
+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<double> 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<double> 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<double> 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<double> 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<double> 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<double> expectedLengthList)
+        {
+            TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList);
+        }
+
+        [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")]
+        private static void TestRowDefinitionsOnly(string length, double containerLength,
+            double expectedDesiredLength, IList<double> expectedLengthList)
+        {
+            // Arrange
+            var layout = new GridLayout(new RowDefinitions(length));
+
+            // Measure - Action & Assert
+            var measure = layout.Measure(containerLength);
+            Assert.Equal(expectedDesiredLength, measure.DesiredLength);
+            Assert.Equal(expectedLengthList, measure.LengthList);
+
+            // Arrange - Action & Assert
+            var arrange = layout.Arrange(containerLength, measure);
+            Assert.Equal(expectedLengthList, arrange.LengthList);
+        }
+
+        [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<double> expectedMeasureList, IList<double> expectedArrangeList)
+        {
+            // Arrange
+            var layout = new GridLayout(new RowDefinitions(length));
+
+            // Measure - Action & Assert
+            var measure = layout.Measure(Inf);
+            Assert.Equal(expectedDesiredLength, measure.DesiredLength);
+            Assert.Equal(expectedMeasureList, measure.LengthList);
+
+            // Arrange - Action & Assert
+            var arrange = layout.Arrange(measure.DesiredLength, measure);
+            Assert.Equal(expectedArrangeList, arrange.LengthList);
+        }
+
+        [Theory]
+        [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })]
+        public void MeasureArrange_ChildHasSize_Correct(string length,
+            IList<double> childLengthList, double containerLength,
+            double expectedDesiredLength, IList<double> expectedLengthList)
+        {
+            // Arrange
+            var lengthList = new ColumnDefinitions(length);
+            var layout = new GridLayout(lengthList);
+            layout.AppendMeasureConventions(
+                Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)),
+                x => childLengthList[x]);
+
+            // Measure - Action & Assert
+            var measure = layout.Measure(containerLength);
+            Assert.Equal(expectedDesiredLength, measure.DesiredLength);
+            Assert.Equal(expectedLengthList, measure.LengthList);
+
+            // Arrange - Action & Assert
+            var arrange = layout.Arrange(containerLength, measure);
+            Assert.Equal(expectedLengthList, arrange.LengthList);
+        }
+
+        [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<double> expectedMeasureLengthList, IList<double> expectedArrangeLengthList)
+        {
+            var length = "100,*,2*";
+            var childLengthList = new[] { 150d, 150d, 150d };
+            var spans = new[] { 1, 2, 1 };
+
+            // Arrange
+            var lengthList = new ColumnDefinitions(length);
+            var layout = new GridLayout(lengthList);
+            layout.AppendMeasureConventions(
+                Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])),
+                x => childLengthList[x]);
+
+            // Measure - Action & Assert
+            var measure = layout.Measure(containerLength);
+            Assert.Equal(expectedDesiredLength, measure.DesiredLength);
+            Assert.Equal(expectedMeasureLengthList, measure.LengthList);
+
+            // Arrange - Action & Assert
+            var arrange = layout.Arrange(
+                double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength,
+                measure);
+            Assert.Equal(expectedArrangeLengthList, arrange.LengthList);
+        }
+    }
+}

+ 124 - 0
tests/Avalonia.Controls.UnitTests/GridMocks.cs

@@ -0,0 +1,124 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    internal static class GridMock
+    {
+        /// <summary>
+        /// Create a mock grid to test its row layout.
+        /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`).
+        /// </summary>
+        /// <param name="measure">The measure height of this grid. PositiveInfinity by default.</param>
+        /// <param name="arrange">The arrange height of this grid. DesiredSize.Height by default.</param>
+        /// <returns>The mock grid that its children bounds will be tested.</returns>
+        internal static Grid New(Size measure = default, Size arrange = default)
+        {
+            var grid = new Grid();
+            grid.Children.Add(new Border());
+            grid.Measure(measure == default ? new Size(double.PositiveInfinity, double.PositiveInfinity) : measure);
+            grid.Arrange(new Rect(default, arrange == default ? grid.DesiredSize : arrange));
+            return grid;
+        }
+
+        /// <summary>
+        /// Create a mock grid to test its row layout.
+        /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`).
+        /// </summary>
+        /// <param name="rows">The row definitions of this mock grid.</param>
+        /// <param name="measure">The measure height of this grid. PositiveInfinity by default.</param>
+        /// <param name="arrange">The arrange height of this grid. DesiredSize.Height by default.</param>
+        /// <returns>The mock grid that its children bounds will be tested.</returns>
+        [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
+        internal static Grid New(RowDefinitions rows,
+            double measure = default, double arrange = default)
+        {
+            var grid = new Grid { RowDefinitions = rows };
+            for (var i = 0; i < rows.Count; i++)
+            {
+                grid.Children.Add(new Border { [Grid.RowProperty] = i });
+            }
+
+            grid.Measure(new Size(double.PositiveInfinity, measure == default ? double.PositiveInfinity : measure));
+            if (arrange == default)
+            {
+                arrange = measure == default ? grid.DesiredSize.Width : measure;
+            }
+
+            grid.Arrange(new Rect(0, 0, 0, arrange));
+
+            return grid;
+        }
+
+        /// <summary>
+        /// Create a mock grid to test its column layout.
+        /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`).
+        /// </summary>
+        /// <param name="columns">The column definitions of this mock grid.</param>
+        /// <param name="measure">The measure width of this grid. PositiveInfinity by default.</param>
+        /// <param name="arrange">The arrange width of this grid. DesiredSize.Width by default.</param>
+        /// <returns>The mock grid that its children bounds will be tested.</returns>
+        [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
+        internal static Grid New(ColumnDefinitions columns,
+            double measure = default, double arrange = default)
+        {
+            var grid = new Grid { ColumnDefinitions = columns };
+            for (var i = 0; i < columns.Count; i++)
+            {
+                grid.Children.Add(new Border { [Grid.ColumnProperty] = i });
+            }
+
+            grid.Measure(new Size(measure == default ? double.PositiveInfinity : measure, double.PositiveInfinity));
+            if (arrange == default)
+            {
+                arrange = measure == default ? grid.DesiredSize.Width : measure;
+            }
+
+            grid.Arrange(new Rect(0, 0, arrange, 0));
+
+            return grid;
+        }
+    }
+
+    internal static class GridAssert
+    {
+        /// <summary>
+        /// Assert all the children heights.
+        /// This method will assume that the grid children count equals row count.
+        /// </summary>
+        /// <param name="grid">The children will be fetched through it.</param>
+        /// <param name="rows">Expected row values of every children.</param>
+        internal static void ChildrenHeight(Grid grid, params double[] rows)
+        {
+            if (grid.Children.Count != rows.Length)
+            {
+                throw new NotSupportedException();
+            }
+
+            for (var i = 0; i < rows.Length; i++)
+            {
+                Assert.Equal(rows[i], grid.Children[i].Bounds.Height);
+            }
+        }
+
+        /// <summary>
+        /// Assert all the children widths.
+        /// This method will assume that the grid children count equals row count.
+        /// </summary>
+        /// <param name="grid">The children will be fetched through it.</param>
+        /// <param name="columns">Expected column values of every children.</param>
+        internal static void ChildrenWidth(Grid grid, params double[] columns)
+        {
+            if (grid.Children.Count != columns.Length)
+            {
+                throw new NotSupportedException();
+            }
+
+            for (var i = 0; i < columns.Length; i++)
+            {
+                Assert.Equal(columns[i], grid.Children[i].Bounds.Width);
+            }
+        }
+    }
+}

+ 91 - 1
tests/Avalonia.Controls.UnitTests/GridTests.cs

@@ -1,7 +1,6 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using Avalonia.Controls;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -64,5 +63,96 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Rect(0, 25, 150, 25), target.Children[1].Bounds);
             Assert.Equal(new Rect(154, 25, 50, 25), target.Children[2].Bounds);
         }
+
+        [Fact]
+        public void Layout_EmptyColumnRow_LayoutLikeANormalPanel()
+        {
+            // Arrange & Action
+            var grid = GridMock.New(arrange: new Size(600, 200));
+
+            // Assert
+            GridAssert.ChildrenWidth(grid, 600);
+            GridAssert.ChildrenHeight(grid, 200);
+        }
+
+        [Fact]
+        public void Layout_PixelRowColumn_BoundsCorrect()
+        {
+            // Arrange & Action
+            var rowGrid = GridMock.New(new RowDefinitions("100,200,300"));
+            var columnGrid = GridMock.New(new ColumnDefinitions("50,100,150"));
+
+            // Assert
+            GridAssert.ChildrenHeight(rowGrid, 100, 200, 300);
+            GridAssert.ChildrenWidth(columnGrid, 50, 100, 150);
+        }
+
+        [Fact]
+        public void Layout_StarRowColumn_BoundsCorrect()
+        {
+            // Arrange & Action
+            var rowGrid = GridMock.New(new RowDefinitions("1*,2*,3*"), 600);
+            var columnGrid = GridMock.New(new ColumnDefinitions("*,*,2*"), 600);
+
+            // Assert
+            GridAssert.ChildrenHeight(rowGrid, 100, 200, 300);
+            GridAssert.ChildrenWidth(columnGrid, 150, 150, 300);
+        }
+
+        [Fact]
+        public void Layout_MixPixelStarRowColumn_BoundsCorrect()
+        {
+            // Arrange & Action
+            var rowGrid = GridMock.New(new RowDefinitions("1*,2*,150"), 600);
+            var columnGrid = GridMock.New(new ColumnDefinitions("1*,2*,150"), 600);
+
+            // Assert
+            GridAssert.ChildrenHeight(rowGrid, 150, 300, 150);
+            GridAssert.ChildrenWidth(columnGrid, 150, 300, 150);
+        }
+
+        [Fact]
+        public void Layout_StarRowColumnWithMinLength_BoundsCorrect()
+        {
+            // Arrange & Action
+            var rowGrid = GridMock.New(new RowDefinitions
+            {
+                new RowDefinition(1, GridUnitType.Star) { MinHeight = 200 },
+                new RowDefinition(1, GridUnitType.Star),
+                new RowDefinition(1, GridUnitType.Star),
+            }, 300);
+            var columnGrid = GridMock.New(new ColumnDefinitions
+            {
+                new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 200 },
+                new ColumnDefinition(1, GridUnitType.Star),
+                new ColumnDefinition(1, GridUnitType.Star),
+            }, 300);
+
+            // Assert
+            GridAssert.ChildrenHeight(rowGrid, 200, 50, 50);
+            GridAssert.ChildrenWidth(columnGrid, 200, 50, 50);
+        }
+
+        [Fact]
+        public void Layout_StarRowColumnWithMaxLength_BoundsCorrect()
+        {
+            // Arrange & Action
+            var rowGrid = GridMock.New(new RowDefinitions
+            {
+                new RowDefinition(1, GridUnitType.Star) { MaxHeight = 200 },
+                new RowDefinition(1, GridUnitType.Star),
+                new RowDefinition(1, GridUnitType.Star),
+            }, 800);
+            var columnGrid = GridMock.New(new ColumnDefinitions
+            {
+                new ColumnDefinition(1, GridUnitType.Star) { MaxWidth = 200 },
+                new ColumnDefinition(1, GridUnitType.Star),
+                new ColumnDefinition(1, GridUnitType.Star),
+            }, 800);
+
+            // Assert
+            GridAssert.ChildrenHeight(rowGrid, 200, 300, 300);
+            GridAssert.ChildrenWidth(columnGrid, 200, 300, 300);
+        }
     }
 }