diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 997e15050f..39c459b307 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -1,6 +1,7 @@  netstandard2.0 + latest false diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 2a564b6a2c..54fcefeb3f 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/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; - /// /// Gets or sets the columns definitions for the grid. /// @@ -183,6 +182,18 @@ namespace Avalonia.Controls element.SetValue(RowSpanProperty, value); } + /// + /// Gets the result of the last column measurement. + /// Use this result to reduce the arrange calculation. + /// + private GridLayout.MeasureResult _columnMeasureCache; + + /// + /// Gets the result of the last row measurement. + /// Use this result to reduce the arrange calculation. + /// + private GridLayout.MeasureResult _rowMeasureCache; + /// /// Measures the grid. /// @@ -190,293 +201,74 @@ namespace Avalonia.Controls /// The desired size of the control. 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()) { - 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 sizes = new List(); - 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(); + 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()) { - // 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); } /// @@ -486,456 +278,138 @@ namespace Avalonia.Controls /// The space taken. 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()) { - 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()) { - 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) + /// + /// 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. + /// + [Pure] + private (Dictionary safeColumns, + Dictionary 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().ToDictionary(child => child, + child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); + var safeRows = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); + return (safeColumns, safeRows); } - private void AllocateDesiredSize(int rowCount, int colCount) + /// + /// 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. + /// + /// The row or column count. + /// The row or column that the user assigned. + /// The rowspan or columnspan that the user assigned. + /// The safe row/column and rowspan/columnspan. + [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; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs new file mode 100644 index 0000000000..10a94a8c82 --- /dev/null +++ b/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 +{ + /// + /// Contains algorithms that can help to measure and arrange a Grid. + /// + internal class GridLayout + { + /// + /// Initialize a new 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. + /// + internal GridLayout([NotNull] ColumnDefinitions columns) + { + if (columns == null) throw new ArgumentNullException(nameof(columns)); + _conventions = columns.Count == 0 + ? new List { new LengthConvention() } + : columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); + } + + /// + /// Initialize a new 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. + /// + internal GridLayout([NotNull] RowDefinitions rows) + { + if (rows == null) throw new ArgumentNullException(nameof(rows)); + _conventions = rows.Count == 0 + ? new List { new LengthConvention() } + : rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); + } + + /// + /// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same. + /// + private const double LayoutTolerance = 1.0 / 256.0; + + /// + /// 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. + /// + [NotNull] + private readonly List _conventions; + + /// + /// Gets all the length conventions that come from the grid children. + /// + [NotNull] + private readonly List _additionalConventions = + new List(); + + /// + /// Appending these elements into the convention list helps lay them out according to their desired sizes. + /// + /// 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. + /// 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 callback. + /// + /// The grid children type. + /// + /// 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. + /// + /// + /// This callback will be called if the thinks that a child should be + /// measured first. Usually, these are the children that have the * or Auto length. + /// + internal void AppendMeasureConventions([NotNull] IDictionary source, + [NotNull] Func 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(); + 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)); + } + } + } + + /// + /// Run measure procedure according to the and gets the . + /// + /// + /// The container length. Usually, it is the constraint of the method. + /// + /// + /// The measured result that containing the desired size and all the column/row lengths. + /// + [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); + } + + /// + /// Run arrange procedure according to the and gets the . + /// + /// + /// The container length. Usually, it is the finalSize of the method. + /// + /// + /// The result that the measuring procedure returns. If it is null, a new measure procedure will run. + /// + /// + /// The measured result that containing the desired size and all the column/row length. + /// + [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); + } + + /// + /// Use the to calculate the fixed length of the Auto column/row. + /// + /// The convention list that all the * with minimum length are fixed. + /// The column/row index that should be fixed. + /// The unit * length for the current rest length. + /// The final length of the Auto length column/row. + [Pure] + private double ApplyAdditionalConventionsForAuto(IReadOnlyList 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); + } + + /// + /// 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. + /// + /// All the conventions that have almost been fixed except the rest *. + /// The total desired length of all the * length. + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + private double AggregateAdditionalConventionsForStars( + IReadOnlyList 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; + } + + /// + /// This method implements the last procedure (M7/7) of measure. + /// It expands all the * length to the fixed length according to the . + /// + /// All the conventions that have almost been fixed except the remaining *. + /// The container length. + /// The final pixel length list. + [Pure] + private static List ExpandStars(IEnumerable conventions, double constraint) + { + // Initial. + var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); + constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); + var starUnitLength = 0.0; + + // 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; + } + + /// + /// 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 . + /// + /// A list of all the column widths and row heights with a fixed pixel length + /// the container length. It can be positive infinity. + private static void Clip([NotNull] IList 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; + } + } + } + + /// + /// Contains the convention of each column/row. + /// This is mostly the same as or . + /// We use this because we can treat the column and the row the same. + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + internal class LengthConvention : ICloneable + { + /// + /// Initialize a new instance of . + /// + public LengthConvention() + { + Length = new GridLength(1.0, GridUnitType.Star); + MinLength = 0.0; + MaxLength = double.PositiveInfinity; + } + + /// + /// Initialize a new instance of . + /// + public LengthConvention(GridLength length, double minLength, double maxLength) + { + Length = length; + MinLength = minLength; + MaxLength = maxLength; + if (length.IsAbsolute) + { + _isFixed = true; + } + } + + /// + /// Gets the of a column or a row. + /// + internal GridLength Length { get; private set; } + + /// + /// Gets the minimum convention for a column or a row. + /// + internal double MinLength { get; } + + /// + /// Gets the maximum convention for a column or a row. + /// + internal double MaxLength { get; } + + /// + /// Fix the . + /// If all columns/rows are fixed, we can get the size of all columns/rows in pixels. + /// + /// + /// The pixel length that should be used to fix the convention. + /// + /// + /// If the convention is pixel length, this exception will throw. + /// + 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; + } + + /// + /// Gets a value that indicates whether this convention is fixed. + /// + private bool _isFixed; + + /// + /// Helps the debugger to display the intermediate column/row calculation result. + /// + private string DebuggerDisplay => + $"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; + + /// + object ICloneable.Clone() => Clone(); + + /// + /// Get a deep copy of this convention list. + /// We need this because we want to store some intermediate states. + /// + internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); + } + + /// + /// 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. + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + internal struct AdditionalLengthConvention + { + /// + /// Initialize a new instance of . + /// + public AdditionalLengthConvention(int index, int span, double min) + { + Index = index; + Span = span; + Min = min; + } + + /// + /// Gets the start index of this additional convention. + /// + public int Index { get; } + + /// + /// Gets the span of this additional convention. + /// + public int Span { get; } + + /// + /// Gets the minimum length of this additional convention. + /// This value is usually provided by the child's desired length. + /// + public double Min { get; } + + /// + /// Helps the debugger to display the intermediate column/row calculation result. + /// + private string DebuggerDisplay => + $"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; + } + + /// + /// Stores the result of the measuring procedure. + /// This result can be used to measure children and assign the desired size. + /// Passing this result to can reduce calculation. + /// + [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] + internal class MeasureResult + { + /// + /// Initialize a new instance of . + /// + internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, + IReadOnlyList leanConventions, IReadOnlyList expandedConventions) + { + ContainerLength = containerLength; + DesiredLength = desiredLength; + GreedyDesiredLength = greedyDesiredLength; + LeanLengthList = leanConventions; + LengthList = expandedConventions; + } + + /// + /// Gets the container length for this result. + /// This property will be used by to determine whether to measure again or not. + /// + public double ContainerLength { get; } + + /// + /// Gets the desired length of this result. + /// Just return this value as the desired size in . + /// + public double DesiredLength { get; } + + /// + /// Gets the desired length if the container has infinite length. + /// + public double GreedyDesiredLength { get; } + + /// + /// Contains the column/row calculation intermediate result. + /// This value is used by for reducing repeat calculation. + /// + public IReadOnlyList LeanLengthList { get; } + + /// + /// Gets the length list for each column/row. + /// + public IReadOnlyList LengthList { get; } + } + + /// + /// Stores the result of the measuring procedure. + /// This result can be used to arrange children and assign the render size. + /// + [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] + internal class ArrangeResult + { + /// + /// Initialize a new instance of . + /// + internal ArrangeResult(IReadOnlyList lengthList) + { + LengthList = lengthList; + } + + /// + /// Gets the length list for each column/row. + /// + public IReadOnlyList LengthList { get; } + } + } +} diff --git a/src/Avalonia.Themes.Default/CalendarItem.xaml b/src/Avalonia.Themes.Default/CalendarItem.xaml index a9756fdb5f..3d3d75a39a 100644 --- a/src/Avalonia.Themes.Default/CalendarItem.xaml +++ b/src/Avalonia.Themes.Default/CalendarItem.xaml @@ -27,7 +27,7 @@ - + diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index fe7e48e085..3706a50525 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,6 +1,7 @@  netcoreapp2.0 + latest Library diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs new file mode 100644 index 0000000000..fbb90de505 --- /dev/null +++ b/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 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); + } + + [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); + 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 expectedMeasureList, IList 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 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 => 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 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); + Assert.Equal(expectedMeasureLengthList, measure.LengthList); + + // Arrange - Action & Assert + var arrange = layout.Arrange( + double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, + measure); + Assert.Equal(expectedArrangeLengthList, arrange.LengthList); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/GridMocks.cs b/tests/Avalonia.Controls.UnitTests/GridMocks.cs new file mode 100644 index 0000000000..23b975207c --- /dev/null +++ b/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 + { + /// + /// Create a mock grid to test its row layout. + /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). + /// + /// The measure height of this grid. PositiveInfinity by default. + /// The arrange height of this grid. DesiredSize.Height by default. + /// The mock grid that its children bounds will be tested. + 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; + } + + /// + /// Create a mock grid to test its row layout. + /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). + /// + /// The row definitions of this mock grid. + /// The measure height of this grid. PositiveInfinity by default. + /// The arrange height of this grid. DesiredSize.Height by default. + /// The mock grid that its children bounds will be tested. + [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; + } + + /// + /// Create a mock grid to test its column layout. + /// This method contains Arrange (`new Grid()`) and Action (`Measure()`/`Arrange()`). + /// + /// The column definitions of this mock grid. + /// The measure width of this grid. PositiveInfinity by default. + /// The arrange width of this grid. DesiredSize.Width by default. + /// The mock grid that its children bounds will be tested. + [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 + { + /// + /// Assert all the children heights. + /// This method will assume that the grid children count equals row count. + /// + /// The children will be fetched through it. + /// Expected row values of every children. + 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); + } + } + + /// + /// Assert all the children widths. + /// This method will assume that the grid children count equals row count. + /// + /// The children will be fetched through it. + /// Expected column values of every children. + 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); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index c5aea6501f..4c79b7775b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/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); + } } }