diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index c321d3a2f1..32331d29ab 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index ad5e96376d..236b478f95 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1,5 +1,5 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; @@ -10,15 +10,81 @@ using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; using Avalonia.VisualTree; +using System.Threading; using JetBrains.Annotations; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia; +using System.Collections; +using Avalonia.Utilities; +using Avalonia.Layout; namespace Avalonia.Controls { /// - /// Lays out child controls according to a grid. + /// Grid /// public class Grid : Panel { + internal bool CellsStructureDirty = true; + internal bool SizeToContentU; + internal bool SizeToContentV; + internal bool HasStarCellsU; + internal bool HasStarCellsV; + internal bool HasGroup3CellsInAutoRows; + internal bool ColumnDefinitionsDirty = true; + internal bool RowDefinitionsDirty = true; + + // index of the first cell in first cell group + internal int CellGroup1; + + // index of the first cell in second cell group + internal int CellGroup2; + + // index of the first cell in third cell group + internal int CellGroup3; + + // index of the first cell in fourth cell group + internal int CellGroup4; + + // temporary array used during layout for various purposes + // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) + private DefinitionBase[] _tempDefinitions; + private GridLinesRenderer _gridLinesRenderer; + + // Keeps track of definition indices. + private int[] _definitionIndices; + + private CellCache[] _cellCache; + + + // Stores unrounded values and rounding errors during layout rounding. + private double[] _roundingErrors; + private DefinitionBase[] _definitionsU; + private DefinitionBase[] _definitionsV; + + // 5 is an arbitrary constant chosen to end the measure loop + private const int _layoutLoopMaxCount = 5; + private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot; + private static readonly IComparer _spanPreferredDistributionOrderComparer; + private static readonly IComparer _spanMaxDistributionOrderComparer; + private static readonly IComparer _minRatioComparer; + private static readonly IComparer _maxRatioComparer; + private static readonly IComparer _starWeightComparer; + + static Grid() + { + ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + + _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + _minRatioComparer = new MinRatioComparer(); + _maxRatioComparer = new MaxRatioComparer(); + _starWeightComparer = new StarWeightComparer(); + } + /// /// Defines the Column attached property. /// @@ -50,91 +116,21 @@ namespace Avalonia.Controls public static readonly AttachedProperty IsSharedSizeScopeProperty = AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - protected override void OnMeasureInvalidated() - { - base.OnMeasureInvalidated(); - _sharedSizeHost?.InvalidateMeasure(this); - } - - private SharedSizeScopeHost _sharedSizeHost; - - /// - /// Defines the SharedSizeScopeHost private property. - /// The ampersands are used to make accessing the property via xaml inconvenient. - /// - internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); - - private ColumnDefinitions _columnDefinitions; - - private RowDefinitions _rowDefinitions; - - static Grid() - { - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); - } - - public Grid() - { - this.AttachedToVisualTree += Grid_AttachedToVisualTree; - this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; - } - /// - /// Gets or sets the columns definitions for the grid. + /// Defines the property. /// - public ColumnDefinitions ColumnDefinitions - { - get - { - if (_columnDefinitions == null) - { - ColumnDefinitions = new ColumnDefinitions(); - } - - return _columnDefinitions; - } - - set - { - if (_columnDefinitions != null) - { - throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); - } - - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); - } - } + public static readonly StyledProperty ShowGridLinesProperty = + AvaloniaProperty.Register( + nameof(ShowGridLines), + defaultValue: true); /// - /// Gets or sets the row definitions for the grid. + /// ShowGridLines property. /// - public RowDefinitions RowDefinitions + public bool ShowGridLines { - get - { - if (_rowDefinitions == null) - { - RowDefinitions = new RowDefinitions(); - } - - return _rowDefinitions; - } - - set - { - if (_rowDefinitions != null) - { - throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); - } - - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); - } + get { return GetValue(ShowGridLinesProperty); } + set { SetValue(ShowGridLinesProperty, value); } } /// @@ -177,7 +173,6 @@ namespace Avalonia.Controls return element.GetValue(RowSpanProperty); } - /// /// Gets the value of the IsSharedSizeScope attached property for a control. /// @@ -228,373 +223,2602 @@ namespace Avalonia.Controls element.SetValue(RowSpanProperty, value); } + private ColumnDefinitions _columnDefinitions; + private RowDefinitions _rowDefinitions; + /// - /// Sets the value of IsSharedSizeScope property for a control. + /// Gets or sets the columns definitions for the grid. /// - /// The control. - /// The IsSharedSizeScope value. - public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) + public ColumnDefinitions ColumnDefinitions { - element.SetValue(IsSharedSizeScopeProperty, value); + get + { + if (_columnDefinitions == null) + { + ColumnDefinitions = new ColumnDefinitions(); + } + + return _columnDefinitions; + } + set + { + _columnDefinitions = value; + _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); + ColumnDefinitionsDirty = true; + + if (_columnDefinitions.Count > 0) + _definitionsU = _columnDefinitions.Cast().ToArray(); + else + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + + _columnDefinitions.CollectionChanged += (_, e) => + { + if (_columnDefinitions.Count == 0) + { + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + } + else + { + _definitionsU = _columnDefinitions.Cast().ToArray(); + ColumnDefinitionsDirty = true; + } + Invalidate(); + }; + } } /// - /// Gets the result of the last column measurement. - /// Use this result to reduce the arrange calculation. + /// Gets or sets the row definitions for the grid. /// - private GridLayout.MeasureResult _columnMeasureCache; + public RowDefinitions RowDefinitions + { + get + { + if (_rowDefinitions == null) + { + RowDefinitions = new RowDefinitions(); + } - /// - /// Gets the result of the last row measurement. - /// Use this result to reduce the arrange calculation. - /// - private GridLayout.MeasureResult _rowMeasureCache; + return _rowDefinitions; + } + set + { + _rowDefinitions = value; + _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - /// - /// Gets the row layout as of the last measure. - /// - private GridLayout _rowLayoutCache; + RowDefinitionsDirty = true; - /// - /// Gets the column layout as of the last measure. - /// - private GridLayout _columnLayoutCache; + if (_rowDefinitions.Count > 0) + _definitionsV = _rowDefinitions.Cast().ToArray(); + else + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; + + _rowDefinitions.CollectionChanged += (_, e) => + { + if (_rowDefinitions.Count == 0) + { + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; + } + else + { + _definitionsV = _rowDefinitions.Cast().ToArray(); + RowDefinitionsDirty = true; + } + Invalidate(); + }; + } + } + + private bool IsTrivialGrid => (_definitionsU?.Length <= 1) && + (_definitionsV?.Length <= 1); /// - /// Measures the grid. + /// Content measurement. /// - /// The available size. - /// The desired size of the control. + /// Constraint + /// Desired size protected override Size MeasureOverride(Size constraint) { - // 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. + Size gridDesiredSize; - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + try { - var maxWidth = 0.0; - var maxHeight = 0.0; - foreach (var child in Children.OfType()) + if (IsTrivialGrid) { - child.Measure(constraint); - maxWidth = Math.Max(maxWidth, child.DesiredSize.Width); - maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + gridDesiredSize = new Size(); + + for (int i = 0, count = Children.Count; i < count; ++i) + { + var child = Children[i]; + if (child != null) + { + child.Measure(constraint); + gridDesiredSize = new Size( + Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); + } + } + } + else + { + { + bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); + bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); + + // Clear index information and rounding errors + if (RowDefinitionsDirty || ColumnDefinitionsDirty) + { + if (_definitionIndices != null) + { + Array.Clear(_definitionIndices, 0, _definitionIndices.Length); + _definitionIndices = null; + } + + if (UseLayoutRounding) + { + if (_roundingErrors != null) + { + Array.Clear(_roundingErrors, 0, _roundingErrors.Length); + _roundingErrors = null; + } + } + } + + ValidateColumnDefinitionsStructure(); + ValidateDefinitionsLayout(_definitionsU, sizeToContentU); + + ValidateRowDefinitionsStructure(); + ValidateDefinitionsLayout(_definitionsV, sizeToContentV); + + CellsStructureDirty |= (SizeToContentU != sizeToContentU) + || (SizeToContentV != sizeToContentV); + + SizeToContentU = sizeToContentU; + SizeToContentV = sizeToContentV; + } + + ValidateCells(); + + Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); + + MeasureCellsGroup(CellGroup1, constraint, false, false); + + { + // after Group1 is measured, only Group3 may have cells belonging to Auto rows. + bool canResolveStarsV = !HasGroup3CellsInAutoRows; + + if (canResolveStarsV) + { + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + MeasureCellsGroup(CellGroup2, constraint, false, false); + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } + MeasureCellsGroup(CellGroup3, constraint, false, false); + } + else + { + // if at least one cell exists in Group2, it must be measured before + // StarsU can be resolved. + bool canResolveStarsU = CellGroup2 > _cellCache.Length; + if (canResolveStarsU) + { + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } + MeasureCellsGroup(CellGroup3, constraint, false, false); + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + } + else + { + // This is a revision to the algorithm employed for the cyclic + // dependency case described above. We now repeatedly + // measure Group3 and Group2 until their sizes settle. We + // also use a count heuristic to break a loop in case of one. + + bool hasDesiredSizeUChanged = false; + int cnt = 0; + + // Cache Group2MinWidths & Group3MinHeights + double[] group2MinSizes = CacheMinSizes(CellGroup2, false); + double[] group3MinSizes = CacheMinSizes(CellGroup3, true); + + MeasureCellsGroup(CellGroup2, constraint, false, true); + + do + { + if (hasDesiredSizeUChanged) + { + // Reset cached Group3Heights + ApplyCachedMinSizes(group3MinSizes, true); + } + + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } + MeasureCellsGroup(CellGroup3, constraint, false, false); + + // Reset cached Group2Widths + ApplyCachedMinSizes(group2MinSizes, false); + + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + MeasureCellsGroup(CellGroup2, constraint, + cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + } + while (hasDesiredSizeUChanged && ++cnt <= _layoutLoopMaxCount); + } + } + } + + MeasureCellsGroup(CellGroup4, constraint, false, false); + + gridDesiredSize = new Size( + CalculateDesiredSize(_definitionsU), + CalculateDesiredSize(_definitionsV)); } - - maxWidth = Math.Min(maxWidth, constraint.Width); - maxHeight = Math.Min(maxHeight, constraint.Height); - return new Size(maxWidth, maxHeight); } - - // 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. - - 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); - - // Calculate measurement. - var columnResult = columnLayout.Measure(constraint.Width); - var rowResult = rowLayout.Measure(constraint.Height); - - // Use the results of the measurement to measure the rest of the children. - foreach (var child in Children.OfType()) + finally { - 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(); - - MeasureOnce(child, new Size(width, height)); } - // Cache the measure result and return the desired size. - _columnMeasureCache = columnResult; - _rowMeasureCache = rowResult; - _rowLayoutCache = rowLayout; - _columnLayoutCache = columnLayout; + return (gridDesiredSize); + } - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + private void ValidateColumnDefinitionsStructure() + { + if (ColumnDefinitionsDirty) { - _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + if (_definitionsU == null) + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + ColumnDefinitionsDirty = false; } + } - return new Size(columnResult.DesiredLength, rowResult.DesiredLength); - - // Measure each child only once. - // If a child has been measured, it will just return the desired size. - Size MeasureOnce(Control child, Size size) + private void ValidateRowDefinitionsStructure() + { + if (RowDefinitionsDirty) { - if (measureCache.TryGetValue(child, out var desiredSize)) - { - return desiredSize; - } + if (_definitionsV == null) + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - child.Measure(size); - desiredSize = child.DesiredSize; - measureCache[child] = desiredSize; - return desiredSize; + RowDefinitionsDirty = false; } } /// - /// Arranges the grid's children. + /// Content arrangement. /// - /// The size allocated to the control. - /// The space taken. - protected override Size ArrangeOverride(Size finalSize) + /// Arrange size + protected override Size ArrangeOverride(Size arrangeSize) { - // 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 (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + try { - foreach (var child in Children.OfType()) + if (IsTrivialGrid) { - child.Arrange(new Rect(finalSize)); + for (int i = 0, count = Children.Count; i < count; ++i) + { + var child = Children[i]; + if (child != null) + { + child.Arrange(new Rect(arrangeSize)); + } + } + } + else + { + Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); + + SetFinalSize(_definitionsU, arrangeSize.Width, true); + SetFinalSize(_definitionsV, arrangeSize.Height, false); + + for (int currentCell = 0; currentCell < _cellCache.Length; ++currentCell) + { + IControl cell = Children[currentCell]; + if (cell == null) + { + continue; + } + + int columnIndex = _cellCache[currentCell].ColumnIndex; + int rowIndex = _cellCache[currentCell].RowIndex; + int columnSpan = _cellCache[currentCell].ColumnSpan; + int rowSpan = _cellCache[currentCell].RowSpan; + + Rect cellRect = new Rect( + columnIndex == 0 ? 0.0 : _definitionsU[columnIndex].FinalOffset, + rowIndex == 0 ? 0.0 : _definitionsV[rowIndex].FinalOffset, + GetFinalSizeForRange(_definitionsU, columnIndex, columnSpan), + GetFinalSizeForRange(_definitionsV, rowIndex, rowSpan)); + + cell.Arrange(cellRect); + } + + // update render bound on grid lines renderer visual + var gridLinesRenderer = EnsureGridLinesRenderer(); + if (gridLinesRenderer != null) + { + gridLinesRenderer.UpdateRenderBounds(arrangeSize); + } } - - return finalSize; - } - - // 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. - - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = _columnLayoutCache; - var rowLayout = _rowLayoutCache; - - var rowCache = _rowMeasureCache; - var columnCache = _columnMeasureCache; - - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { - (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); - - rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); - columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); } - - // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); - var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); - // Arrange the children. - foreach (var child in Children.OfType()) + finally { - 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]); - child.Arrange(new Rect(x, y, width, height)); + SetValid(); } - // Assign the actual width. for (var i = 0; i < ColumnDefinitions.Count; i++) { - ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; + ColumnDefinitions[i].ActualWidth = GetFinalColumnDefinitionWidth(i); } - // Assign the actual height. for (var i = 0; i < RowDefinitions.Count; i++) { - RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; + RowDefinitions[i].ActualHeight = GetFinalRowDefinitionHeight(i); } - // Return the render size. - return finalSize; + return (arrangeSize); } /// - /// Tests whether this grid belongs to a shared size scope. + /// Returns final width for a column. /// - /// True if the grid is registered in a shared size scope. - internal bool HasSharedSizeScope() + /// + /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. + /// + internal double GetFinalColumnDefinitionWidth(int columnIndex) { - return _sharedSizeHost != null; + double value = 0.0; + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionsDirty) + { + value = _definitionsU[(columnIndex + 1) % _definitionsU.Length].FinalOffset; + if (columnIndex != 0) { value -= _definitionsU[columnIndex].FinalOffset; } + } + return (value); } /// - /// Called when the SharedSizeScope for a given grid has changed. - /// Unregisters the grid from it's current scope and finds a new one (if any) + /// Returns final height for a row. /// /// - /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. + /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. /// - internal void SharedScopeChanged() + internal double GetFinalRowDefinitionHeight(int rowIndex) { - _sharedSizeHost?.UnegisterGrid(this); - - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + double value = 0.0; - if (scope != null) + // actual value calculations require structure to be up-to-date + if (!RowDefinitionsDirty) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + value = _definitionsV[(rowIndex + 1) % _definitionsV.Length].FinalOffset; + if (rowIndex != 0) { value -= _definitionsV[rowIndex].FinalOffset; } } + return (value); + } + /// + /// Invalidates grid caches and makes the grid dirty for measure. + /// + internal void Invalidate() + { + CellsStructureDirty = true; InvalidateMeasure(); } /// - /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid - /// in it. + /// Lays out cells according to rows and columns, and creates lookup grids. /// - /// The source of the event. - /// The event arguments. - private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private void ValidateCells() { - var scope = - new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + if (!CellsStructureDirty) return; - if (_sharedSizeHost != null) - throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + _cellCache = new CellCache[Children.Count]; + CellGroup1 = int.MaxValue; + CellGroup2 = int.MaxValue; + CellGroup3 = int.MaxValue; + CellGroup4 = int.MaxValue; - if (scope != null) + bool hasStarCellsU = false; + bool hasStarCellsV = false; + bool hasGroup3CellsInAutoRows = false; + + for (int i = _cellCache.Length - 1; i >= 0; --i) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + var child = Children[i] as Control; + + if (child == null) + { + continue; + } + + var cell = new CellCache(); + + // read indices from the corresponding properties + // clamp to value < number_of_columns + // column >= 0 is guaranteed by property value validation callback + cell.ColumnIndex = Math.Min(GetColumn(child), _definitionsU.Length - 1); + + // clamp to value < number_of_rows + // row >= 0 is guaranteed by property value validation callback + cell.RowIndex = Math.Min(GetRow(child), _definitionsV.Length - 1); + + // read span properties + // clamp to not exceed beyond right side of the grid + // column_span > 0 is guaranteed by property value validation callback + cell.ColumnSpan = Math.Min(GetColumnSpan(child), _definitionsU.Length - cell.ColumnIndex); + + // clamp to not exceed beyond bottom side of the grid + // row_span > 0 is guaranteed by property value validation callback + cell.RowSpan = Math.Min(GetRowSpan(child), _definitionsV.Length - cell.RowIndex); + + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < _definitionsU.Length); + Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < _definitionsV.Length); + + // + // calculate and cache length types for the child + // + cell.SizeTypeU = GetLengthTypeForRange(_definitionsU, cell.ColumnIndex, cell.ColumnSpan); + cell.SizeTypeV = GetLengthTypeForRange(_definitionsV, cell.RowIndex, cell.RowSpan); + + hasStarCellsU |= cell.IsStarU; + hasStarCellsV |= cell.IsStarV; + + // + // distribute cells into four groups. + // + if (!cell.IsStarV) + { + if (!cell.IsStarU) + { + cell.Next = CellGroup1; + CellGroup1 = i; + } + else + { + cell.Next = CellGroup3; + CellGroup3 = i; + + // remember if this cell belongs to auto row + hasGroup3CellsInAutoRows |= cell.IsAutoV; + } + } + else + { + if (cell.IsAutoU + // note below: if spans through Star column it is NOT Auto + && !cell.IsStarU) + { + cell.Next = CellGroup2; + CellGroup2 = i; + } + else + { + cell.Next = CellGroup4; + CellGroup4 = i; + } + } + + _cellCache[i] = cell; } + + HasStarCellsU = hasStarCellsU; + HasStarCellsV = hasStarCellsV; + HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; + + CellsStructureDirty = false; } /// - /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. + /// Validates layout time size type information on given array of definitions. + /// Sets MinSize and MeasureSizes. /// - /// The source of the event. - /// The event arguments. - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + /// Array of definitions to update. + /// if "true" then star definitions are treated as Auto. + private void ValidateDefinitionsLayout( + DefinitionBase[] definitions, + bool treatStarAsAuto) { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } + for (int i = 0; i < definitions.Length; ++i) + { + // Reset minimum size. + definitions[i].MinSize = 0; + double userMinSize = definitions[i].UserMinSize; + double userMaxSize = definitions[i].UserMaxSize; + double userSize = 0; - /// - /// 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() - { - 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); + switch (definitions[i].UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + definitions[i].SizeType = LayoutTimeSizeType.Pixel; + userSize = definitions[i].UserSize.Value; + + // this was brought with NewLayout and defeats squishy behavior + userMinSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + break; + case (GridUnitType.Auto): + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + break; + case (GridUnitType.Star): + if (treatStarAsAuto) + { + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + } + else + { + definitions[i].SizeType = LayoutTimeSizeType.Star; + userSize = double.PositiveInfinity; + } + break; + default: + Debug.Assert(false); + break; + } + + definitions[i].UpdateMinSize(userMinSize); + definitions[i].MeasureSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + } } - /// - /// 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) + private double[] CacheMinSizes(int cellsHead, bool isRows) { - var index = userIndex; - var span = userSpan; + double[] minSizes = isRows ? new double[_definitionsV.Length] + : new double[_definitionsU.Length]; - if (index < 0) + for (int j = 0; j < minSizes.Length; j++) { - span = index + span; - index = 0; + minSizes[j] = -1; } - if (span <= 0) + int i = cellsHead; + do { - span = 1; - } + if (isRows) + { + minSizes[_cellCache[i].RowIndex] = _definitionsV[_cellCache[i].RowIndex].MinSize; + } + else + { + minSizes[_cellCache[i].ColumnIndex] = _definitionsU[_cellCache[i].ColumnIndex].MinSize; + } - if (userIndex >= length) - { - index = length - 1; - span = 1; - } - else if (userIndex + userSpan > length) + i = _cellCache[i].Next; + } while (i < _cellCache.Length); + + return minSizes; + } + + private void ApplyCachedMinSizes(double[] minSizes, bool isRows) + { + for (int i = 0; i < minSizes.Length; i++) { - span = length - userIndex; + if (MathUtilities.GreaterThanOrClose(minSizes[i], 0)) + { + if (isRows) + { + _definitionsV[i].MinSize = minSizes[i]; + } + else + { + _definitionsU[i].MinSize = minSizes[i]; + } + } } + } - return (index, span); + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV) + { + bool unusedHasDesiredSizeUChanged; + MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, + forceInfinityV, out unusedHasDesiredSizeUChanged); } - private static int ValidateColumn(AvaloniaObject o, int value) + /// + /// Measures one group of cells. + /// + /// Head index of the cells chain. + /// Reference size for spanned cells + /// calculations. + /// When "true" cells' desired + /// width is not registered in columns. + /// Passed through to MeasureCell. + /// When "true" cells' desired height is not registered in rows. + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV, + out bool hasDesiredSizeUChanged) { - if (value < 0) + hasDesiredSizeUChanged = false; + + if (cellsHead >= _cellCache.Length) { - throw new ArgumentException("Invalid Grid.Column value."); + return; } - return value; + Hashtable spanStore = null; + bool ignoreDesiredSizeV = forceInfinityV; + + int i = cellsHead; + do + { + double oldWidth = Children[i].DesiredSize.Width; + + MeasureCell(i, forceInfinityV); + + hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, Children[i].DesiredSize.Width); + + if (!ignoreDesiredSizeU) + { + if (_cellCache[i].ColumnSpan == 1) + { + _definitionsU[_cellCache[i].ColumnIndex] + .UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, + _definitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + _cellCache[i].ColumnIndex, + _cellCache[i].ColumnSpan, + true, + Children[i].DesiredSize.Width); + } + } + + if (!ignoreDesiredSizeV) + { + if (_cellCache[i].RowSpan == 1) + { + _definitionsV[_cellCache[i].RowIndex] + .UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, + _definitionsV[_cellCache[i].RowIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + _cellCache[i].RowIndex, + _cellCache[i].RowSpan, + false, + Children[i].DesiredSize.Height); + } + } + + i = _cellCache[i].Next; + } while (i < _cellCache.Length); + + if (spanStore != null) + { + foreach (DictionaryEntry e in spanStore) + { + SpanKey key = (SpanKey)e.Key; + double requestedSize = (double)e.Value; + + EnsureMinSizeInDefinitionRange( + key.U ? _definitionsU : _definitionsV, + key.Start, + key.Count, + requestedSize, + key.U ? referenceSize.Width : referenceSize.Height); + } + } } - private static int ValidateRow(AvaloniaObject o, int value) + /// + /// Helper method to register a span information for delayed processing. + /// + /// Reference to a hashtable object used as storage. + /// Span starting index. + /// Span count. + /// true if this is a column span. false if this is a row span. + /// Value to store. If an entry already exists the biggest value is stored. + private static void RegisterSpan( + ref Hashtable store, + int start, + int count, + bool u, + double value) { - if (value < 0) + if (store == null) { - throw new ArgumentException("Invalid Grid.Row value."); + store = new Hashtable(); } - return value; + SpanKey key = new SpanKey(start, count, u); + object o = store[key]; + + if (o == null + || value > (double)o) + { + store[key] = value; + } } /// - /// Called when the value of changes for a control. + /// Takes care of measuring a single cell. /// - /// The control that triggered the change. - /// Change arguments. - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + /// Index of the cell to measure. + /// If "true" then cell is always + /// calculated to infinite height. + private void MeasureCell( + int cell, + bool forceInfinityV) { - var shouldDispose = (arg2.OldValue is bool d) && d; - if (shouldDispose) + double cellMeasureWidth; + double cellMeasureHeight; + + if (_cellCache[cell].IsAutoU + && !_cellCache[cell].IsStarU) { - var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; - if (host == null) - throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); - host.Dispose(); - source.ClearValue(s_sharedSizeScopeHostProperty); + // if cell belongs to at least one Auto column and not a single Star column + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureWidth = double.PositiveInfinity; } - - var shouldAssign = (arg2.NewValue is bool a) && a; - if (shouldAssign) + else { - if (source.GetValue(s_sharedSizeScopeHostProperty) != null) - throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); - source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost()); + // otherwise... + cellMeasureWidth = GetMeasureSizeForRange( + _definitionsU, + _cellCache[cell].ColumnIndex, + _cellCache[cell].ColumnSpan); } - // if the scope has changed, notify the descendant grids that they need to update. - if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) + if (forceInfinityV) { - var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); + cellMeasureHeight = double.PositiveInfinity; + } + else if (_cellCache[cell].IsAutoV + && !_cellCache[cell].IsStarV) + { + // if cell belongs to at least one Auto row and not a single Star row + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureHeight = double.PositiveInfinity; + } + else + { + cellMeasureHeight = GetMeasureSizeForRange( + _definitionsV, + _cellCache[cell].RowIndex, + _cellCache[cell].RowSpan); + } + + var child = Children[cell]; - foreach (var grid in participatingGrids) - grid.SharedScopeChanged(); + if (child != null) + { + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); } } + + /// + /// Calculates one dimensional measure size for given definitions' range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Calculated measure size. + /// + /// For "Auto" definitions MinWidth is used in place of PreferredSize. + /// + private double GetMeasureSizeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); + + double measureSize = 0; + int i = start + count - 1; + + do + { + measureSize += (definitions[i].SizeType == LayoutTimeSizeType.Auto) + ? definitions[i].MinSize + : definitions[i].MeasureSize; + } while (--i >= start); + + return (measureSize); + } + + /// + /// Accumulates length type information for given definition's range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Length type for given range. + private LayoutTimeSizeType GetLengthTypeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); + + LayoutTimeSizeType lengthType = LayoutTimeSizeType.None; + int i = start + count - 1; + + do + { + lengthType |= definitions[i].SizeType; + } while (--i >= start); + + return (lengthType); + } + + /// + /// Distributes min size back to definition array's range. + /// + /// Start of the range. + /// Number of items in the range. + /// Minimum size that should "fit" into the definitions range. + /// Definition array receiving distribution. + /// Size used to resolve percentages. + private void EnsureMinSizeInDefinitionRange( + DefinitionBase[] definitions, + int start, + int count, + double requestedSize, + double percentReferenceSize) + { + Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Length); + + // avoid processing when asked to distribute "0" + if (!MathUtilities.IsZero(requestedSize)) + { + DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting + int end = start + count; + int autoDefinitionsCount = 0; + double rangeMinSize = 0; + double rangePreferredSize = 0; + double rangeMaxSize = 0; + double maxMaxSize = 0; // maximum of maximum sizes + + // first accumulate the necessary information: + // a) sum up the sizes in the range; + // b) count the number of auto definitions in the range; + // c) initialize temp array + // d) cache the maximum size into SizeCache + // e) accumulate max of max sizes + for (int i = start; i < end; ++i) + { + double minSize = definitions[i].MinSize; + double preferredSize = definitions[i].PreferredSize; + double maxSize = Math.Max(definitions[i].UserMaxSize, minSize); + + rangeMinSize += minSize; + rangePreferredSize += preferredSize; + rangeMaxSize += maxSize; + + definitions[i].SizeCache = maxSize; + + // sanity check: no matter what, but min size must always be the smaller; + // max size must be the biggest; and preferred should be in between + Debug.Assert(minSize <= preferredSize + && preferredSize <= maxSize + && rangeMinSize <= rangePreferredSize + && rangePreferredSize <= rangeMaxSize); + + if (maxMaxSize < maxSize) maxMaxSize = maxSize; + if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; + tempDefinitions[i - start] = definitions[i]; + } + + // avoid processing if the range already big enough + if (requestedSize > rangeMinSize) + { + if (requestedSize <= rangePreferredSize) + { + // + // requestedSize fits into preferred size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions - they should continue to stay "tight"; + // * for all non-auto definitions distribute to equi-size min sizes, without exceeding preferred size. + // + // in order to achieve that, definitions are sorted in a way that all auto definitions + // are first, then definitions follow ascending order with PreferredSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, _spanPreferredDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + // adjust sizeToDistribute value by subtracting auto definition min size + sizeToDistribute -= (tempDefinitions[i].MinSize); + } + + for (; i < count; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); + if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } + sizeToDistribute -= newMinSize; + } + + // sanity check: requested size must all be distributed + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); + } + else if (requestedSize <= rangeMaxSize) + { + // + // requestedSize bigger than preferred size, but fit into max size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions, if possible - they should continue to stay "tight"; + // * for all non-auto definitions distribute to euqi-size min sizes, without exceeding max size. + // + // in order to achieve that, definitions are sorted in a way that all non-auto definitions + // are last, then definitions follow ascending order with MaxSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, _spanMaxDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].PreferredSize; + double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + for (; i < count; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].MinSize; + double newMinSize = preferredSize + sizeToDistribute / (count - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + // sanity check: requested size must all be distributed + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); + } + else + { + // + // requestedSize bigger than max size of the range. + // distribute according to the following logic: + // * for all definitions distribute to equi-size min sizes. + // + double equalSize = requestedSize / count; + + if (equalSize < maxMaxSize + && !MathUtilities.AreClose(equalSize, maxMaxSize)) + { + // equi-size is less than maximum of maxSizes. + // in this case distribute so that smaller definitions grow faster than + // bigger ones. + double totalRemainingSize = maxMaxSize * count - rangeMaxSize; + double sizeToDistribute = requestedSize - rangeMaxSize; + + // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers + Debug.Assert(!double.IsInfinity(totalRemainingSize) + && !double.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !double.IsNaN(sizeToDistribute) + && sizeToDistribute > 0); + + for (int i = 0; i < count; ++i) + { + double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + } + } + else + { + // + // equi-size is greater or equal to maximum of max sizes. + // all definitions receive equalSize as their mim sizes. + // + for (int i = 0; i < count; ++i) + { + tempDefinitions[i].UpdateMinSize(equalSize); + } + } + } + } + } + } + + // new implementation as of 4.7. Several improvements: + // 1. Allocate to *-defs hitting their min or max constraints, before allocating + // to other *-defs. A def that hits its min uses more space than its + // proportional share, reducing the space available to everyone else. + // The legacy algorithm deducted this space only from defs processed + // after the min; the new algorithm deducts it proportionally from all + // defs. This avoids the "*-defs exceed available space" problem, + // and other related problems where *-defs don't receive proportional + // allocations even though no constraints are preventing it. + // 2. When multiple defs hit min or max, resolve the one with maximum + // discrepancy (defined below). This avoids discontinuities - small + // change in available space resulting in large change to one def's allocation. + // 3. Correct handling of large *-values, including Infinity. + + /// + /// Resolves Star's for given array of definitions. + /// + /// Array of definitions to resolve stars. + /// All available size. + /// + /// Must initialize LayoutSize for all Star entries in given array of definitions. + /// + private void ResolveStar( + DefinitionBase[] definitions, + double availableSize) + { + int defCount = definitions.Length; + DefinitionBase[] tempDefinitions = TempDefinitions; + int minCount = 0, maxCount = 0; + double takenSize = 0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.SizeType == LayoutTimeSizeType.Star) + { + ++starCount; + def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" + if (def.UserSize.Value > maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3 = true; runPhase2and3;) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of tempDefinitions, + // the "max" list in the second half. TempDefinitions has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + switch (def.SizeType) + { + case (LayoutTimeSizeType.Auto): + takenSize += definitions[i].MinSize; + break; + case (LayoutTimeSizeType.Pixel): + takenSize += def.MeasureSize; + break; + case (LayoutTimeSizeType.Star): + if (def.MeasureSize < 0.0) + { + takenSize += -def.MeasureSize; // already resolved + } + else + { + double starWeight = StarWeight(def, scale); + totalStarWeight += starWeight; + + if (def.MinSize > 0.0) + { + // store ratio w/min in MeasureSize (for now) + tempDefinitions[minCount++] = def; + def.MeasureSize = starWeight / def.MinSize; + } + + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + if (!double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + tempDefinitions[defCount + maxCount++] = def; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + break; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = availableSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + Array.Sort(tempDefinitions, 0, minCount, _minRatioComparer); + Array.Sort(tempDefinitions, defCount, maxCount, _maxRatioComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.SizeType == LayoutTimeSizeType.Star && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedDef = tempDefinitions[minCount - 1]; + resolvedSize = resolvedDef.MinSize; + --minCount; + } + else + { + resolvedDef = tempDefinitions[defCount + maxCount - 1]; + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = availableSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) + { + --minCount; + tempDefinitions[minCount] = null; + } + while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) + { + --maxCount; + tempDefinitions[defCount + maxCount] = null; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < availableSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > availableSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[defCount + i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.SizeType == LayoutTimeSizeType.Star) + { + if (def.MeasureSize < 0.0) + { + // this def was resolved in phase 3 - fix up its measure size + def.MeasureSize = -def.MeasureSize; + } + else + { + // this def needs resolution, add it to the list, sorted by *-weight + tempDefinitions[starCount++] = def; + def.MeasureSize = StarWeight(def, scale); + } + } + } + + if (starCount > 0) + { + Array.Sort(tempDefinitions, 0, starCount, _starWeightComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = tempDefinitions[i]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = tempDefinitions[i]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSize, resolvedSize); + + def.MeasureSize = resolvedSize; + takenSize += resolvedSize; + } + } + } + + /// + /// Calculates desired size for given array of definitions. + /// + /// Array of definitions to use for calculations. + /// Desired size. + private double CalculateDesiredSize( + DefinitionBase[] definitions) + { + double desiredSize = 0; + + for (int i = 0; i < definitions.Length; ++i) + { + desiredSize += definitions[i].MinSize; + } + + return (desiredSize); + } + + /// + /// Calculates and sets final size for all definitions in the given array. + /// + /// Array of definitions to process. + /// Final size to lay out to. + /// True if sizing row definitions, false for columns + private void SetFinalSize( + DefinitionBase[] definitions, + double finalSize, + bool columns) + { + int defCount = definitions.Length; + int[] definitionIndices = DefinitionIndices; + int minCount = 0, maxCount = 0; + double takenSize = 0.0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + ++starCount; + def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" + if (def.UserSize.Value > maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3 = true; runPhase2and3;) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of definitionIndices, + // the "max" list in the second half. DefinitionIndices has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + // Debug.Assert(!def.IsShared, "*-defs cannot be shared"); + + if (def.MeasureSize < 0.0) + { + takenSize += -def.MeasureSize; // already resolved + } + else + { + double starWeight = StarWeight(def, scale); + totalStarWeight += starWeight; + + if (def.MinSize > 0.0) + { + // store ratio w/min in MeasureSize (for now) + definitionIndices[minCount++] = i; + def.MeasureSize = starWeight / def.MinSize; + } + + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + if (!double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + definitionIndices[defCount + maxCount++] = i; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + } + else + { + double userSize = 0; + + switch (def.UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + userSize = def.UserSize.Value; + break; + + case (GridUnitType.Auto): + userSize = def.MinSize; + break; + } + + double userMaxSize; + + // if (def.IsShared) + // { + // // overriding userMaxSize effectively prevents squishy-ness. + // // this is a "solution" to avoid shared definitions from been sized to + // // different final size at arrange time, if / when different grids receive + // // different final sizes. + // userMaxSize = userSize; + // } + // else + // { + userMaxSize = def.UserMaxSize; + // } + + def.SizeCache = Math.Max(def.MinSize, Math.Min(userSize, userMaxSize)); + takenSize += def.SizeCache; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = finalSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + + MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); + Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); + MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); + Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.UserSize.IsStar && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? definitions[definitionIndices[defCount + maxCount - 1]].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + int resolvedIndex; + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedIndex = definitionIndices[minCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = resolvedDef.MinSize; + --minCount; + } + else + { + resolvedIndex = definitionIndices[defCount + maxCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = finalSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && definitions[definitionIndices[minCount - 1]].MeasureSize < 0.0) + { + --minCount; + definitionIndices[minCount] = -1; + } + while (maxCount > 0 && definitions[definitionIndices[defCount + maxCount - 1]].MeasureSize < 0.0) + { + --maxCount; + definitionIndices[defCount + maxCount] = -1; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < finalSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + if (definitionIndices[i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > finalSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + if (definitionIndices[defCount + i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[defCount + i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + if (def.MeasureSize < 0.0) + { + // this def was resolved in phase 3 - fix up its size + def.SizeCache = -def.MeasureSize; + } + else + { + // this def needs resolution, add it to the list, sorted by *-weight + definitionIndices[starCount++] = i; + def.MeasureSize = StarWeight(def, scale); + } + } + } + + if (starCount > 0) + { + StarWeightIndexComparer starWeightIndexComparer = new StarWeightIndexComparer(definitions); + Array.Sort(definitionIndices, 0, starCount, starWeightIndexComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight. + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(finalSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSize, resolvedSize); + + // Use the raw (unrounded) sizes to update takenSize, so that + // proportions are computed in the same terms as in phase 3; + // this avoids errors arising from min/max constraints. + takenSize += resolvedSize; + def.SizeCache = resolvedSize; + } + } + + // Phase 5. Apply layout rounding. We do this after fully allocating + // unrounded sizes, to avoid breaking assumptions in the previous phases + if (UseLayoutRounding) + { + var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; + + double[] roundingErrors = RoundingErrors; + double roundedTakenSize = 0.0; + + // round each of the allocated sizes, keeping track of the deltas + for (int i = 0; i < definitions.Length; ++i) + { + DefinitionBase def = definitions[i]; + double roundedSize = RoundLayoutValue(def.SizeCache, dpi); + roundingErrors[i] = (roundedSize - def.SizeCache); + def.SizeCache = roundedSize; + roundedTakenSize += roundedSize; + } + + // The total allocation might differ from finalSize due to rounding + // effects. Tweak the allocations accordingly. + + // Theoretical and historical note. The problem at hand - allocating + // space to columns (or rows) with *-weights, min and max constraints, + // and layout rounding - has a long history. Especially the special + // case of 50 columns with min=1 and available space=435 - allocating + // seats in the U.S. House of Representatives to the 50 states in + // proportion to their population. There are numerous algorithms + // and papers dating back to the 1700's, including the book: + // Balinski, M. and H. Young, Fair Representation, Yale University Press, New Haven, 1982. + // + // One surprising result of all this research is that *any* algorithm + // will suffer from one or more undesirable features such as the + // "population paradox" or the "Alabama paradox", where (to use our terminology) + // increasing the available space by one pixel might actually decrease + // the space allocated to a given column, or increasing the weight of + // a column might decrease its allocation. This is worth knowing + // in case someone complains about this behavior; it's not a bug so + // much as something inherent to the problem. Cite the book mentioned + // above or one of the 100s of references, and resolve as WontFix. + // + // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) + // each being allocated a large number of pixels (~50 or greater), and + // people don't even notice the kind of 1-pixel anomolies that are + // theoretically inevitable, or don't care if they do. At least they shouldn't + // care - no one should be using the results WPF's grid layout to make + // quantitative decisions; its job is to produce a reasonable display, not + // to allocate seats in Congress. + // + // Our algorithm is more susceptible to paradox than the one currently + // used for Congressional allocation ("Huntington-Hill" algorithm), but + // it is faster to run: O(N log N) vs. O(S * N), where N=number of + // definitions, S = number of available pixels. And it produces + // adequate results in practice, as mentioned above. + // + // To reiterate one point: all this only applies when layout rounding + // is in effect. When fractional sizes are allowed, the algorithm + // behaves as well as possible, subject to the min/max constraints + // and precision of floating-point computation. (However, the resulting + // display is subject to anti-aliasing problems. TANSTAAFL.) + + if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) + { + // Compute deltas + for (int i = 0; i < definitions.Length; ++i) + { + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); + double adjustedSize = roundedTakenSize; + double dpiIncrement = 1.0 / dpi; + + if (roundedTakenSize > finalSize) + { + int i = definitions.Length - 1; + while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSize); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (roundedTakenSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Length) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSize); + if (final > definition.SizeCache) + { + adjustedSize += dpiIncrement; + } + definition.SizeCache = final; + i++; + } + } + } + } + + // Phase 6. Compute final offsets + definitions[0].FinalOffset = 0.0; + for (int i = 0; i < definitions.Length; ++i) + { + definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; + } + } + + // Choose the ratio with maximum discrepancy from the current proportion. + // Returns: + // true if proportion fails a min constraint but not a max, or + // if the min constraint has higher discrepancy + // false if proportion fails a max constraint but not a min, or + // if the max constraint has higher discrepancy + // null if proportion doesn't fail a min or max constraint + // The discrepancy is the ratio of the proportion to the max- or min-ratio. + // When both ratios hit the constraint, minRatio < proportion < maxRatio, + // and the minRatio has higher discrepancy if + // (proportion / minRatio) > (maxRatio / proportion) + private static bool? Choose(double minRatio, double maxRatio, double proportion) + { + if (minRatio < proportion) + { + if (maxRatio > proportion) + { + // compare proportion/minRatio : maxRatio/proportion, but + // do it carefully to avoid floating-point overflow or underflow + // and divide-by-0. + double minPower = Math.Floor(Math.Log(minRatio, 2.0)); + double maxPower = Math.Floor(Math.Log(maxRatio, 2.0)); + double f = Math.Pow(2.0, Math.Floor((minPower + maxPower) / 2.0)); + if ((proportion / f) * (proportion / f) > (minRatio / f) * (maxRatio / f)) + { + return true; + } + else + { + return false; + } + } + else + { + return true; + } + } + else if (maxRatio > proportion) + { + return false; + } + + return null; + } + + /// + /// Sorts row/column indices by rounding error if layout rounding is applied. + /// + /// Index, rounding error pair + /// Index, rounding error pair + /// 1 if x.Value > y.Value, 0 if equal, -1 otherwise + private static int CompareRoundingErrors(KeyValuePair x, KeyValuePair y) + { + if (x.Value < y.Value) + { + return -1; + } + else if (x.Value > y.Value) + { + return 1; + } + return 0; + } + + /// + /// Calculates final (aka arrange) size for given range. + /// + /// Array of definitions to process. + /// Start of the range. + /// Number of items in the range. + /// Final size. + private double GetFinalSizeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + double size = 0; + int i = start + count - 1; + + do + { + size += definitions[i].SizeCache; + } while (--i >= start); + + return (size); + } + + /// + /// Clears dirty state for the grid and its columns / rows + /// + private void SetValid() + { + if (IsTrivialGrid) + { + if (_tempDefinitions != null) + { + // TempDefinitions has to be cleared to avoid "memory leaks" + Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); + _tempDefinitions = null; + } + } + } + + + /// + /// Synchronized ShowGridLines property with the state of the grid's visual collection + /// by adding / removing GridLinesRenderer visual. + /// Returns a reference to GridLinesRenderer visual or null. + /// + private GridLinesRenderer EnsureGridLinesRenderer() + { + // + // synchronize the state + // + if (ShowGridLines && (_gridLinesRenderer == null)) + { + _gridLinesRenderer = new GridLinesRenderer(); + this.VisualChildren.Add(_gridLinesRenderer); + } + + if ((!ShowGridLines) && (_gridLinesRenderer != null)) + { + this.VisualChildren.Remove(_gridLinesRenderer); + _gridLinesRenderer = null; + } + + return (_gridLinesRenderer); + } + + private double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!MathUtilities.AreClose(dpiScale, 1.0)) + { + newValue = Math.Round(value * dpiScale) / dpiScale; + // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. + if (double.IsNaN(newValue) || + double.IsInfinity(newValue) || + MathUtilities.AreClose(newValue, double.MaxValue)) + { + newValue = value; + } + } + else + { + newValue = Math.Round(value); + } + + return newValue; + } + + + private static int ValidateColumn(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Column value."); + } + + return value; + } + + private static int ValidateRow(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Row value."); + } + + return value; + } + + private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) + { + if (!grid.IsTrivialGrid) // trivial grid is 1 by 1. there is no grid lines anyway + { + grid.Invalidate(); + } + } + + /// + /// Helper for Comparer methods. + /// + /// + /// true if one or both of x and y are null, in which case result holds + /// the relative sort order. + /// + private static bool CompareNullRefs(object x, object y, out int result) + { + result = 2; + + if (x == null) + { + if (y == null) + { + result = 0; + } + else + { + result = -1; + } + } + else + { + if (y == null) + { + result = 1; + } + } + + return (result != 2); + } + + /// + /// Helper accessor to layout time array of definitions. + /// + private DefinitionBase[] TempDefinitions + { + get + { + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; + + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) + { + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); + if (tempDefinitionsWeakRef == null) + { + _tempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); + } + else + { + _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) + { + _tempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = _tempDefinitions; + } + } + } + return (_tempDefinitions); + } + } + + /// + /// Helper accessor to definition indices. + /// + private int[] DefinitionIndices + { + get + { + int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; + + if (_definitionIndices == null || _definitionIndices.Length < requiredLength) + { + _definitionIndices = new int[requiredLength]; + } + + return _definitionIndices; + } + } + + /// + /// Helper accessor to rounding errors. + /// + private double[] RoundingErrors + { + get + { + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); + + if (_roundingErrors == null && requiredLength == 0) + { + _roundingErrors = new double[1]; + } + else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) + { + _roundingErrors = new double[requiredLength]; + } + return _roundingErrors; + } + } + + /// + /// Returns *-weight, adjusted for scale computed during Phase 1 + /// + static double StarWeight(DefinitionBase def, double scale) + { + if (scale < 0.0) + { + // if one of the *-weights is Infinity, adjust the weights by mapping + // Infinty to 1.0 and everything else to 0.0: the infinite items share the + // available space equally, everyone else gets nothing. + return (double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; + } + else + { + return def.UserSize.Value * scale; + } + } + + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } + + /// + /// CellCache stored calculated values of + /// 1. attached cell positioning properties; + /// 2. size type; + /// 3. index of a next cell in the group; + /// + private struct CellCache + { + internal int ColumnIndex; + internal int RowIndex; + internal int ColumnSpan; + internal int RowSpan; + internal LayoutTimeSizeType SizeTypeU; + internal LayoutTimeSizeType SizeTypeV; + internal int Next; + internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } + internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } + } + + /// + /// Helper class for representing a key for a span in hashtable. + /// + private class SpanKey + { + /// + /// Constructor. + /// + /// Starting index of the span. + /// Span count. + /// true for columns; false for rows. + internal SpanKey(int start, int count, bool u) + { + _start = start; + _count = count; + _u = u; + } + + /// + /// + /// + public override int GetHashCode() + { + int hash = (_start ^ (_count << 2)); + + if (_u) hash &= 0x7ffffff; + else hash |= 0x8000000; + + return (hash); + } + + /// + /// + /// + public override bool Equals(object obj) + { + SpanKey sk = obj as SpanKey; + return (sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u); + } + + /// + /// Returns start index of the span. + /// + internal int Start { get { return (_start); } } + + /// + /// Returns span count. + /// + internal int Count { get { return (_count); } } + + /// + /// Returns true if this is a column span. + /// false if this is a row span. + /// + internal bool U { get { return (_u); } } + + private int _start; + private int _count; + private bool _u; + } + + /// + /// SpanPreferredDistributionOrderComparer. + /// + private class SpanPreferredDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.MinSize.CompareTo(definitionY.MinSize); + } + else + { + result = -1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = +1; + } + else + { + result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); + } + } + } + + return result; + } + } + + /// + /// SpanMaxDistributionOrderComparer. + /// + private class SpanMaxDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + else + { + result = +1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = -1; + } + else + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + } + } + + return result; + } + } + + /// + /// RoundingErrorIndexComparer. + /// + private class RoundingErrorIndexComparer : IComparer + { + private readonly double[] errors; + + internal RoundingErrorIndexComparer(double[] errors) + { + Contract.Requires(errors != null); + this.errors = errors; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + int result; + + if (!CompareNullRefs(indexX, indexY, out result)) + { + double errorX = errors[indexX.Value]; + double errorY = errors[indexY.Value]; + result = errorX.CompareTo(errorY); + } + + return result; + } + } + + /// + /// MinRatioComparer. + /// Sort by w/min (stored in MeasureSize), descending. + /// We query the list from the back, i.e. in ascending order of w/min. + /// + private class MinRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } + + /// + /// MaxRatioComparer. + /// Sort by w/max (stored in SizeCache), ascending. + /// We query the list from the back, i.e. in descending order of w/max. + /// + private class MaxRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// StarWeightComparer. + /// Sort by *-weight (stored in MeasureSize), ascending. + /// + private class StarWeightComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } + + /// + /// MinRatioIndexComparer. + /// + private class MinRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MinRatioIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } + + /// + /// MaxRatioIndexComparer. + /// + private class MaxRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MaxRatioIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// MaxRatioIndexComparer. + /// + private class StarWeightIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal StarWeightIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } + + /// + /// Helper to render grid lines. + /// + private class GridLinesRenderer : Control + { + /// + /// Static initialization + /// + static GridLinesRenderer() + { + var oddDashArray = new List(); + oddDashArray.Add(_dashLength); + oddDashArray.Add(_dashLength); + var ds1 = new DashStyle(oddDashArray, 0); + _oddDashPen = new Pen(Brushes.Blue, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); + + var evenDashArray = new List(); + evenDashArray.Add(_dashLength); + evenDashArray.Add(_dashLength); + var ds2 = new DashStyle(evenDashArray, 0); + _evenDashPen = new Pen(Brushes.Yellow, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); + } + + /// + /// UpdateRenderBounds. + /// + public override void Render(DrawingContext drawingContext) + { + var grid = this.GetVisualParent(); + + if (grid == null + || !grid.ShowGridLines + || grid.IsTrivialGrid) + { + return; + } + + for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].ActualWidth, 0.0, + grid.ColumnDefinitions[i].ActualWidth, _lastArrangeSize.Height); + } + + for (int i = 1; i < grid.RowDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].ActualHeight, + _lastArrangeSize.Width, grid.RowDefinitions[i].ActualHeight); + } + } + + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) + { + var start = new Point(startX, startY); + var end = new Point(endX, endY); + drawingContext.DrawLine(_oddDashPen, start, end); + drawingContext.DrawLine(_evenDashPen, start, end); + } + + internal void UpdateRenderBounds(Size arrangeSize) + { + _lastArrangeSize = arrangeSize; + this.InvalidateVisual(); + } + + private static Size _lastArrangeSize; + private const double _dashLength = 4.0; // + private const double _penWidth = 1.0; // + private static readonly Pen _oddDashPen; // first pen to draw dash + private static readonly Pen _evenDashPen; // second pen to draw dash + } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs deleted file mode 100644 index 35a38a6423..0000000000 --- a/src/Avalonia.Controls/GridWPF.cs +++ /dev/null @@ -1,2824 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.VisualTree; -using System.Threading; -using JetBrains.Annotations; -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia; -using System.Collections; -using Avalonia.Utilities; -using Avalonia.Layout; - -namespace Avalonia.Controls -{ - /// - /// Grid - /// - public class Grid : Panel - { - internal bool CellsStructureDirty = true; - internal bool SizeToContentU; - internal bool SizeToContentV; - internal bool HasStarCellsU; - internal bool HasStarCellsV; - internal bool HasGroup3CellsInAutoRows; - internal bool ColumnDefinitionsDirty = true; - internal bool RowDefinitionsDirty = true; - - // index of the first cell in first cell group - internal int CellGroup1; - - // index of the first cell in second cell group - internal int CellGroup2; - - // index of the first cell in third cell group - internal int CellGroup3; - - // index of the first cell in fourth cell group - internal int CellGroup4; - - // temporary array used during layout for various purposes - // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) - internal DefinitionBase[] _tempDefinitions; - private GridLinesRenderer _gridLinesRenderer; - - // Keeps track of definition indices. - private int[] _definitionIndices; - - private CellCache[] _cellCache; - - - // Stores unrounded values and rounding errors during layout rounding. - private double[] _roundingErrors; - private DefinitionBase[] _definitionsU; - private DefinitionBase[] _definitionsV; - - // 5 is an arbitrary constant chosen to end the measure loop - private const int _layoutLoopMaxCount = 5; - private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot; - private static readonly IComparer _spanPreferredDistributionOrderComparer; - private static readonly IComparer _spanMaxDistributionOrderComparer; - private static readonly IComparer _minRatioComparer; - private static readonly IComparer _maxRatioComparer; - private static readonly IComparer _starWeightComparer; - - static Grid() - { - ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - - _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); - _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); - _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); - _minRatioComparer = new MinRatioComparer(); - _maxRatioComparer = new MaxRatioComparer(); - _starWeightComparer = new StarWeightComparer(); - } - - /// - /// Defines the Column attached property. - /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached( - "Column", - validate: ValidateColumn); - - /// - /// Defines the ColumnSpan attached property. - /// - public static readonly AttachedProperty ColumnSpanProperty = - AvaloniaProperty.RegisterAttached("ColumnSpan", 1); - - /// - /// Defines the Row attached property. - /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached( - "Row", - validate: ValidateRow); - - /// - /// Defines the RowSpan attached property. - /// - public static readonly AttachedProperty RowSpanProperty = - AvaloniaProperty.RegisterAttached("RowSpan", 1); - - public static readonly AttachedProperty IsSharedSizeScopeProperty = - AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShowGridLinesProperty = - AvaloniaProperty.Register( - nameof(ShowGridLines), - defaultValue: false); - - /// - /// ShowGridLines property. - /// - public bool ShowGridLines - { - get { return GetValue(ShowGridLinesProperty); } - set { SetValue(ShowGridLinesProperty, value); } - } - - /// - /// Gets the value of the Column attached property for a control. - /// - /// The control. - /// The control's column. - public static int GetColumn(AvaloniaObject element) - { - return element.GetValue(ColumnProperty); - } - - /// - /// Gets the value of the ColumnSpan attached property for a control. - /// - /// The control. - /// The control's column span. - public static int GetColumnSpan(AvaloniaObject element) - { - return element.GetValue(ColumnSpanProperty); - } - - /// - /// Gets the value of the Row attached property for a control. - /// - /// The control. - /// The control's row. - public static int GetRow(AvaloniaObject element) - { - return element.GetValue(RowProperty); - } - - /// - /// Gets the value of the RowSpan attached property for a control. - /// - /// The control. - /// The control's row span. - public static int GetRowSpan(AvaloniaObject element) - { - return element.GetValue(RowSpanProperty); - } - - /// - /// Gets the value of the IsSharedSizeScope attached property for a control. - /// - /// The control. - /// The control's IsSharedSizeScope value. - public static bool GetIsSharedSizeScope(AvaloniaObject element) - { - return element.GetValue(IsSharedSizeScopeProperty); - } - - /// - /// Sets the value of the Column attached property for a control. - /// - /// The control. - /// The column value. - public static void SetColumn(AvaloniaObject element, int value) - { - element.SetValue(ColumnProperty, value); - } - - /// - /// Sets the value of the ColumnSpan attached property for a control. - /// - /// The control. - /// The column span value. - public static void SetColumnSpan(AvaloniaObject element, int value) - { - element.SetValue(ColumnSpanProperty, value); - } - - /// - /// Sets the value of the Row attached property for a control. - /// - /// The control. - /// The row value. - public static void SetRow(AvaloniaObject element, int value) - { - element.SetValue(RowProperty, value); - } - - /// - /// Sets the value of the RowSpan attached property for a control. - /// - /// The control. - /// The row span value. - public static void SetRowSpan(AvaloniaObject element, int value) - { - element.SetValue(RowSpanProperty, value); - } - - private ColumnDefinitions _columnDefinitions; - private RowDefinitions _rowDefinitions; - - /// - /// Gets or sets the columns definitions for the grid. - /// - public ColumnDefinitions ColumnDefinitions - { - get - { - if (_columnDefinitions == null) - { - ColumnDefinitions = new ColumnDefinitions(); - } - - return _columnDefinitions; - } - set - { - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - ColumnDefinitionsDirty = true; - - if (_columnDefinitions.Count > 0) - _definitionsU = _columnDefinitions.Cast().ToArray(); - else - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - - _columnDefinitions.CollectionChanged += (_, e) => - { - if (_columnDefinitions.Count == 0) - { - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - } - else - { - _definitionsU = _columnDefinitions.Cast().ToArray(); - ColumnDefinitionsDirty = true; - } - Invalidate(); - }; - } - } - - /// - /// Gets or sets the row definitions for the grid. - /// - public RowDefinitions RowDefinitions - { - get - { - if (_rowDefinitions == null) - { - RowDefinitions = new RowDefinitions(); - } - - return _rowDefinitions; - } - set - { - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - - RowDefinitionsDirty = true; - - if (_rowDefinitions.Count > 0) - _definitionsV = _rowDefinitions.Cast().ToArray(); - else - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - - _rowDefinitions.CollectionChanged += (_, e) => - { - if (_rowDefinitions.Count == 0) - { - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - } - else - { - _definitionsV = _rowDefinitions.Cast().ToArray(); - RowDefinitionsDirty = true; - } - Invalidate(); - }; - } - } - - private bool IsTrivialGrid => (_definitionsU?.Length <= 1) && - (_definitionsV?.Length <= 1); - - /// - /// Content measurement. - /// - /// Constraint - /// Desired size - protected override Size MeasureOverride(Size constraint) - { - Size gridDesiredSize; - - try - { - if (IsTrivialGrid) - { - gridDesiredSize = new Size(); - - for (int i = 0, count = Children.Count; i < count; ++i) - { - var child = Children[i]; - if (child != null) - { - child.Measure(constraint); - gridDesiredSize = new Size( - Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), - Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); - } - } - } - else - { - { - bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); - bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); - - // Clear index information and rounding errors - if (RowDefinitionsDirty || ColumnDefinitionsDirty) - { - if (_definitionIndices != null) - { - Array.Clear(_definitionIndices, 0, _definitionIndices.Length); - _definitionIndices = null; - } - - if (UseLayoutRounding) - { - if (_roundingErrors != null) - { - Array.Clear(_roundingErrors, 0, _roundingErrors.Length); - _roundingErrors = null; - } - } - } - - ValidateColumnDefinitionsStructure(); - ValidateDefinitionsLayout(_definitionsU, sizeToContentU); - - ValidateRowDefinitionsStructure(); - ValidateDefinitionsLayout(_definitionsV, sizeToContentV); - - CellsStructureDirty |= (SizeToContentU != sizeToContentU) - || (SizeToContentV != sizeToContentV); - - SizeToContentU = sizeToContentU; - SizeToContentV = sizeToContentV; - } - - ValidateCells(); - - Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); - - MeasureCellsGroup(CellGroup1, constraint, false, false); - - { - // after Group1 is measured, only Group3 may have cells belonging to Auto rows. - bool canResolveStarsV = !HasGroup3CellsInAutoRows; - - if (canResolveStarsV) - { - if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, false, false); - if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } - MeasureCellsGroup(CellGroup3, constraint, false, false); - } - else - { - // if at least one cell exists in Group2, it must be measured before - // StarsU can be resolved. - bool canResolveStarsU = CellGroup2 > _cellCache.Length; - if (canResolveStarsU) - { - if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } - MeasureCellsGroup(CellGroup3, constraint, false, false); - if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } - } - else - { - // This is a revision to the algorithm employed for the cyclic - // dependency case described above. We now repeatedly - // measure Group3 and Group2 until their sizes settle. We - // also use a count heuristic to break a loop in case of one. - - bool hasDesiredSizeUChanged = false; - int cnt = 0; - - // Cache Group2MinWidths & Group3MinHeights - double[] group2MinSizes = CacheMinSizes(CellGroup2, false); - double[] group3MinSizes = CacheMinSizes(CellGroup3, true); - - MeasureCellsGroup(CellGroup2, constraint, false, true); - - do - { - if (hasDesiredSizeUChanged) - { - // Reset cached Group3Heights - ApplyCachedMinSizes(group3MinSizes, true); - } - - if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } - MeasureCellsGroup(CellGroup3, constraint, false, false); - - // Reset cached Group2Widths - ApplyCachedMinSizes(group2MinSizes, false); - - if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, - cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); - } - while (hasDesiredSizeUChanged && ++cnt <= _layoutLoopMaxCount); - } - } - } - - MeasureCellsGroup(CellGroup4, constraint, false, false); - - gridDesiredSize = new Size( - CalculateDesiredSize(_definitionsU), - CalculateDesiredSize(_definitionsV)); - } - } - finally - { - } - - return (gridDesiredSize); - } - - private void ValidateColumnDefinitionsStructure() - { - if (ColumnDefinitionsDirty) - { - if (_definitionsU == null) - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - ColumnDefinitionsDirty = false; - } - } - - private void ValidateRowDefinitionsStructure() - { - if (RowDefinitionsDirty) - { - if (_definitionsV == null) - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - - RowDefinitionsDirty = false; - } - } - - /// - /// Content arrangement. - /// - /// Arrange size - protected override Size ArrangeOverride(Size arrangeSize) - { - try - { - if (IsTrivialGrid) - { - for (int i = 0, count = Children.Count; i < count; ++i) - { - var child = Children[i]; - if (child != null) - { - child.Arrange(new Rect(arrangeSize)); - } - } - } - else - { - Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); - - SetFinalSize(_definitionsU, arrangeSize.Width, true); - SetFinalSize(_definitionsV, arrangeSize.Height, false); - - for (int currentCell = 0; currentCell < _cellCache.Length; ++currentCell) - { - IControl cell = Children[currentCell]; - if (cell == null) - { - continue; - } - - int columnIndex = _cellCache[currentCell].ColumnIndex; - int rowIndex = _cellCache[currentCell].RowIndex; - int columnSpan = _cellCache[currentCell].ColumnSpan; - int rowSpan = _cellCache[currentCell].RowSpan; - - Rect cellRect = new Rect( - columnIndex == 0 ? 0.0 : _definitionsU[columnIndex].FinalOffset, - rowIndex == 0 ? 0.0 : _definitionsV[rowIndex].FinalOffset, - GetFinalSizeForRange(_definitionsU, columnIndex, columnSpan), - GetFinalSizeForRange(_definitionsV, rowIndex, rowSpan)); - - cell.Arrange(cellRect); - } - - // update render bound on grid lines renderer visual - var gridLinesRenderer = EnsureGridLinesRenderer(); - if (gridLinesRenderer != null) - { - gridLinesRenderer.UpdateRenderBounds(arrangeSize); - } - } - } - finally - { - SetValid(); - } - - for (var i = 0; i < ColumnDefinitions.Count; i++) - { - ColumnDefinitions[i].ActualWidth = GetFinalColumnDefinitionWidth(i); - } - - for (var i = 0; i < RowDefinitions.Count; i++) - { - RowDefinitions[i].ActualHeight = GetFinalRowDefinitionHeight(i); - } - - return (arrangeSize); - } - - /// - /// Returns final width for a column. - /// - /// - /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. - /// - internal double GetFinalColumnDefinitionWidth(int columnIndex) - { - double value = 0.0; - - // actual value calculations require structure to be up-to-date - if (!ColumnDefinitionsDirty) - { - value = _definitionsU[(columnIndex + 1) % _definitionsU.Length].FinalOffset; - if (columnIndex != 0) { value -= _definitionsU[columnIndex].FinalOffset; } - } - return (value); - } - - /// - /// Returns final height for a row. - /// - /// - /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. - /// - internal double GetFinalRowDefinitionHeight(int rowIndex) - { - double value = 0.0; - - // actual value calculations require structure to be up-to-date - if (!RowDefinitionsDirty) - { - value = _definitionsV[(rowIndex + 1) % _definitionsV.Length].FinalOffset; - if (rowIndex != 0) { value -= _definitionsV[rowIndex].FinalOffset; } - } - return (value); - } - - /// - /// Invalidates grid caches and makes the grid dirty for measure. - /// - internal void Invalidate() - { - CellsStructureDirty = true; - InvalidateMeasure(); - } - - /// - /// Lays out cells according to rows and columns, and creates lookup grids. - /// - private void ValidateCells() - { - if (!CellsStructureDirty) return; - - _cellCache = new CellCache[Children.Count]; - CellGroup1 = int.MaxValue; - CellGroup2 = int.MaxValue; - CellGroup3 = int.MaxValue; - CellGroup4 = int.MaxValue; - - bool hasStarCellsU = false; - bool hasStarCellsV = false; - bool hasGroup3CellsInAutoRows = false; - - for (int i = _cellCache.Length - 1; i >= 0; --i) - { - var child = Children[i] as Control; - - if (child == null) - { - continue; - } - - var cell = new CellCache(); - - // read indices from the corresponding properties - // clamp to value < number_of_columns - // column >= 0 is guaranteed by property value validation callback - cell.ColumnIndex = Math.Min(GetColumn(child), _definitionsU.Length - 1); - - // clamp to value < number_of_rows - // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(GetRow(child), _definitionsV.Length - 1); - - // read span properties - // clamp to not exceed beyond right side of the grid - // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(GetColumnSpan(child), _definitionsU.Length - cell.ColumnIndex); - - // clamp to not exceed beyond bottom side of the grid - // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(GetRowSpan(child), _definitionsV.Length - cell.RowIndex); - - Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < _definitionsU.Length); - Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < _definitionsV.Length); - - // - // calculate and cache length types for the child - // - cell.SizeTypeU = GetLengthTypeForRange(_definitionsU, cell.ColumnIndex, cell.ColumnSpan); - cell.SizeTypeV = GetLengthTypeForRange(_definitionsV, cell.RowIndex, cell.RowSpan); - - hasStarCellsU |= cell.IsStarU; - hasStarCellsV |= cell.IsStarV; - - // - // distribute cells into four groups. - // - if (!cell.IsStarV) - { - if (!cell.IsStarU) - { - cell.Next = CellGroup1; - CellGroup1 = i; - } - else - { - cell.Next = CellGroup3; - CellGroup3 = i; - - // remember if this cell belongs to auto row - hasGroup3CellsInAutoRows |= cell.IsAutoV; - } - } - else - { - if (cell.IsAutoU - // note below: if spans through Star column it is NOT Auto - && !cell.IsStarU) - { - cell.Next = CellGroup2; - CellGroup2 = i; - } - else - { - cell.Next = CellGroup4; - CellGroup4 = i; - } - } - - _cellCache[i] = cell; - } - - HasStarCellsU = hasStarCellsU; - HasStarCellsV = hasStarCellsV; - HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; - - CellsStructureDirty = false; - } - - /// - /// Validates layout time size type information on given array of definitions. - /// Sets MinSize and MeasureSizes. - /// - /// Array of definitions to update. - /// if "true" then star definitions are treated as Auto. - private void ValidateDefinitionsLayout( - DefinitionBase[] definitions, - bool treatStarAsAuto) - { - for (int i = 0; i < definitions.Length; ++i) - { - // Reset minimum size. - definitions[i].MinSize = 0; - - double userMinSize = definitions[i].UserMinSize; - double userMaxSize = definitions[i].UserMaxSize; - double userSize = 0; - - switch (definitions[i].UserSize.GridUnitType) - { - case (GridUnitType.Pixel): - definitions[i].SizeType = LayoutTimeSizeType.Pixel; - userSize = definitions[i].UserSize.Value; - - // this was brought with NewLayout and defeats squishy behavior - userMinSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); - break; - case (GridUnitType.Auto): - definitions[i].SizeType = LayoutTimeSizeType.Auto; - userSize = double.PositiveInfinity; - break; - case (GridUnitType.Star): - if (treatStarAsAuto) - { - definitions[i].SizeType = LayoutTimeSizeType.Auto; - userSize = double.PositiveInfinity; - } - else - { - definitions[i].SizeType = LayoutTimeSizeType.Star; - userSize = double.PositiveInfinity; - } - break; - default: - Debug.Assert(false); - break; - } - - definitions[i].UpdateMinSize(userMinSize); - definitions[i].MeasureSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); - } - } - - private double[] CacheMinSizes(int cellsHead, bool isRows) - { - double[] minSizes = isRows ? new double[_definitionsV.Length] - : new double[_definitionsU.Length]; - - for (int j = 0; j < minSizes.Length; j++) - { - minSizes[j] = -1; - } - - int i = cellsHead; - do - { - if (isRows) - { - minSizes[_cellCache[i].RowIndex] = _definitionsV[_cellCache[i].RowIndex].MinSize; - } - else - { - minSizes[_cellCache[i].ColumnIndex] = _definitionsU[_cellCache[i].ColumnIndex].MinSize; - } - - i = _cellCache[i].Next; - } while (i < _cellCache.Length); - - return minSizes; - } - - private void ApplyCachedMinSizes(double[] minSizes, bool isRows) - { - for (int i = 0; i < minSizes.Length; i++) - { - if (MathUtilities.GreaterThanOrClose(minSizes[i], 0)) - { - if (isRows) - { - _definitionsV[i].MinSize = minSizes[i]; - } - else - { - _definitionsU[i].MinSize = minSizes[i]; - } - } - } - } - - private void MeasureCellsGroup( - int cellsHead, - Size referenceSize, - bool ignoreDesiredSizeU, - bool forceInfinityV) - { - bool unusedHasDesiredSizeUChanged; - MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, - forceInfinityV, out unusedHasDesiredSizeUChanged); - } - - /// - /// Measures one group of cells. - /// - /// Head index of the cells chain. - /// Reference size for spanned cells - /// calculations. - /// When "true" cells' desired - /// width is not registered in columns. - /// Passed through to MeasureCell. - /// When "true" cells' desired height is not registered in rows. - private void MeasureCellsGroup( - int cellsHead, - Size referenceSize, - bool ignoreDesiredSizeU, - bool forceInfinityV, - out bool hasDesiredSizeUChanged) - { - hasDesiredSizeUChanged = false; - - if (cellsHead >= _cellCache.Length) - { - return; - } - - Hashtable spanStore = null; - bool ignoreDesiredSizeV = forceInfinityV; - - int i = cellsHead; - do - { - double oldWidth = Children[i].DesiredSize.Width; - - MeasureCell(i, forceInfinityV); - - hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, Children[i].DesiredSize.Width); - - if (!ignoreDesiredSizeU) - { - if (_cellCache[i].ColumnSpan == 1) - { - _definitionsU[_cellCache[i].ColumnIndex] - .UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, - _definitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); - } - else - { - RegisterSpan( - ref spanStore, - _cellCache[i].ColumnIndex, - _cellCache[i].ColumnSpan, - true, - Children[i].DesiredSize.Width); - } - } - - if (!ignoreDesiredSizeV) - { - if (_cellCache[i].RowSpan == 1) - { - _definitionsV[_cellCache[i].RowIndex] - .UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, - _definitionsV[_cellCache[i].RowIndex].UserMaxSize)); - } - else - { - RegisterSpan( - ref spanStore, - _cellCache[i].RowIndex, - _cellCache[i].RowSpan, - false, - Children[i].DesiredSize.Height); - } - } - - i = _cellCache[i].Next; - } while (i < _cellCache.Length); - - if (spanStore != null) - { - foreach (DictionaryEntry e in spanStore) - { - SpanKey key = (SpanKey)e.Key; - double requestedSize = (double)e.Value; - - EnsureMinSizeInDefinitionRange( - key.U ? _definitionsU : _definitionsV, - key.Start, - key.Count, - requestedSize, - key.U ? referenceSize.Width : referenceSize.Height); - } - } - } - - /// - /// Helper method to register a span information for delayed processing. - /// - /// Reference to a hashtable object used as storage. - /// Span starting index. - /// Span count. - /// true if this is a column span. false if this is a row span. - /// Value to store. If an entry already exists the biggest value is stored. - private static void RegisterSpan( - ref Hashtable store, - int start, - int count, - bool u, - double value) - { - if (store == null) - { - store = new Hashtable(); - } - - SpanKey key = new SpanKey(start, count, u); - object o = store[key]; - - if (o == null - || value > (double)o) - { - store[key] = value; - } - } - - /// - /// Takes care of measuring a single cell. - /// - /// Index of the cell to measure. - /// If "true" then cell is always - /// calculated to infinite height. - private void MeasureCell( - int cell, - bool forceInfinityV) - { - double cellMeasureWidth; - double cellMeasureHeight; - - if (_cellCache[cell].IsAutoU - && !_cellCache[cell].IsStarU) - { - // if cell belongs to at least one Auto column and not a single Star column - // then it should be calculated "to content", thus it is possible to "shortcut" - // calculations and simply assign PositiveInfinity here. - cellMeasureWidth = double.PositiveInfinity; - } - else - { - // otherwise... - cellMeasureWidth = GetMeasureSizeForRange( - _definitionsU, - _cellCache[cell].ColumnIndex, - _cellCache[cell].ColumnSpan); - } - - if (forceInfinityV) - { - cellMeasureHeight = double.PositiveInfinity; - } - else if (_cellCache[cell].IsAutoV - && !_cellCache[cell].IsStarV) - { - // if cell belongs to at least one Auto row and not a single Star row - // then it should be calculated "to content", thus it is possible to "shortcut" - // calculations and simply assign PositiveInfinity here. - cellMeasureHeight = double.PositiveInfinity; - } - else - { - cellMeasureHeight = GetMeasureSizeForRange( - _definitionsV, - _cellCache[cell].RowIndex, - _cellCache[cell].RowSpan); - } - - var child = Children[cell]; - - if (child != null) - { - Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); - child.Measure(childConstraint); - } - } - - /// - /// Calculates one dimensional measure size for given definitions' range. - /// - /// Source array of definitions to read values from. - /// Starting index of the range. - /// Number of definitions included in the range. - /// Calculated measure size. - /// - /// For "Auto" definitions MinWidth is used in place of PreferredSize. - /// - private double GetMeasureSizeForRange( - DefinitionBase[] definitions, - int start, - int count) - { - Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); - - double measureSize = 0; - int i = start + count - 1; - - do - { - measureSize += (definitions[i].SizeType == LayoutTimeSizeType.Auto) - ? definitions[i].MinSize - : definitions[i].MeasureSize; - } while (--i >= start); - - return (measureSize); - } - - /// - /// Accumulates length type information for given definition's range. - /// - /// Source array of definitions to read values from. - /// Starting index of the range. - /// Number of definitions included in the range. - /// Length type for given range. - private LayoutTimeSizeType GetLengthTypeForRange( - DefinitionBase[] definitions, - int start, - int count) - { - Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); - - LayoutTimeSizeType lengthType = LayoutTimeSizeType.None; - int i = start + count - 1; - - do - { - lengthType |= definitions[i].SizeType; - } while (--i >= start); - - return (lengthType); - } - - /// - /// Distributes min size back to definition array's range. - /// - /// Start of the range. - /// Number of items in the range. - /// Minimum size that should "fit" into the definitions range. - /// Definition array receiving distribution. - /// Size used to resolve percentages. - private void EnsureMinSizeInDefinitionRange( - DefinitionBase[] definitions, - int start, - int count, - double requestedSize, - double percentReferenceSize) - { - Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Length); - - // avoid processing when asked to distribute "0" - if (!MathUtilities.IsZero(requestedSize)) - { - DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting - int end = start + count; - int autoDefinitionsCount = 0; - double rangeMinSize = 0; - double rangePreferredSize = 0; - double rangeMaxSize = 0; - double maxMaxSize = 0; // maximum of maximum sizes - - // first accumulate the necessary information: - // a) sum up the sizes in the range; - // b) count the number of auto definitions in the range; - // c) initialize temp array - // d) cache the maximum size into SizeCache - // e) accumulate max of max sizes - for (int i = start; i < end; ++i) - { - double minSize = definitions[i].MinSize; - double preferredSize = definitions[i].PreferredSize; - double maxSize = Math.Max(definitions[i].UserMaxSize, minSize); - - rangeMinSize += minSize; - rangePreferredSize += preferredSize; - rangeMaxSize += maxSize; - - definitions[i].SizeCache = maxSize; - - // sanity check: no matter what, but min size must always be the smaller; - // max size must be the biggest; and preferred should be in between - Debug.Assert(minSize <= preferredSize - && preferredSize <= maxSize - && rangeMinSize <= rangePreferredSize - && rangePreferredSize <= rangeMaxSize); - - if (maxMaxSize < maxSize) maxMaxSize = maxSize; - if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; - tempDefinitions[i - start] = definitions[i]; - } - - // avoid processing if the range already big enough - if (requestedSize > rangeMinSize) - { - if (requestedSize <= rangePreferredSize) - { - // - // requestedSize fits into preferred size of the range. - // distribute according to the following logic: - // * do not distribute into auto definitions - they should continue to stay "tight"; - // * for all non-auto definitions distribute to equi-size min sizes, without exceeding preferred size. - // - // in order to achieve that, definitions are sorted in a way that all auto definitions - // are first, then definitions follow ascending order with PreferredSize as the key of sorting. - // - double sizeToDistribute; - int i; - - Array.Sort(tempDefinitions, 0, count, _spanPreferredDistributionOrderComparer); - for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) - { - // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); - - // adjust sizeToDistribute value by subtracting auto definition min size - sizeToDistribute -= (tempDefinitions[i].MinSize); - } - - for (; i < count; ++i) - { - // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); - - double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); - if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } - sizeToDistribute -= newMinSize; - } - - // sanity check: requested size must all be distributed - Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); - } - else if (requestedSize <= rangeMaxSize) - { - // - // requestedSize bigger than preferred size, but fit into max size of the range. - // distribute according to the following logic: - // * do not distribute into auto definitions, if possible - they should continue to stay "tight"; - // * for all non-auto definitions distribute to euqi-size min sizes, without exceeding max size. - // - // in order to achieve that, definitions are sorted in a way that all non-auto definitions - // are last, then definitions follow ascending order with MaxSize as the key of sorting. - // - double sizeToDistribute; - int i; - - Array.Sort(tempDefinitions, 0, count, _spanMaxDistributionOrderComparer); - for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) - { - // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); - - double preferredSize = tempDefinitions[i].PreferredSize; - double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); - } - - for (; i < count; ++i) - { - // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); - - double preferredSize = tempDefinitions[i].MinSize; - double newMinSize = preferredSize + sizeToDistribute / (count - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); - } - - // sanity check: requested size must all be distributed - Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); - } - else - { - // - // requestedSize bigger than max size of the range. - // distribute according to the following logic: - // * for all definitions distribute to equi-size min sizes. - // - double equalSize = requestedSize / count; - - if (equalSize < maxMaxSize - && !MathUtilities.AreClose(equalSize, maxMaxSize)) - { - // equi-size is less than maximum of maxSizes. - // in this case distribute so that smaller definitions grow faster than - // bigger ones. - double totalRemainingSize = maxMaxSize * count - rangeMaxSize; - double sizeToDistribute = requestedSize - rangeMaxSize; - - // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers - Debug.Assert(!double.IsInfinity(totalRemainingSize) - && !double.IsNaN(totalRemainingSize) - && totalRemainingSize > 0 - && !double.IsInfinity(sizeToDistribute) - && !double.IsNaN(sizeToDistribute) - && sizeToDistribute > 0); - - for (int i = 0; i < count; ++i) - { - double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; - tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); - } - } - else - { - // - // equi-size is greater or equal to maximum of max sizes. - // all definitions receive equalSize as their mim sizes. - // - for (int i = 0; i < count; ++i) - { - tempDefinitions[i].UpdateMinSize(equalSize); - } - } - } - } - } - } - - // new implementation as of 4.7. Several improvements: - // 1. Allocate to *-defs hitting their min or max constraints, before allocating - // to other *-defs. A def that hits its min uses more space than its - // proportional share, reducing the space available to everyone else. - // The legacy algorithm deducted this space only from defs processed - // after the min; the new algorithm deducts it proportionally from all - // defs. This avoids the "*-defs exceed available space" problem, - // and other related problems where *-defs don't receive proportional - // allocations even though no constraints are preventing it. - // 2. When multiple defs hit min or max, resolve the one with maximum - // discrepancy (defined below). This avoids discontinuities - small - // change in available space resulting in large change to one def's allocation. - // 3. Correct handling of large *-values, including Infinity. - - /// - /// Resolves Star's for given array of definitions. - /// - /// Array of definitions to resolve stars. - /// All available size. - /// - /// Must initialize LayoutSize for all Star entries in given array of definitions. - /// - private void ResolveStar( - DefinitionBase[] definitions, - double availableSize) - { - int defCount = definitions.Length; - DefinitionBase[] tempDefinitions = TempDefinitions; - int minCount = 0, maxCount = 0; - double takenSize = 0; - double totalStarWeight = 0.0; - int starCount = 0; // number of unresolved *-definitions - double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" - - // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights - double maxStar = 0.0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.SizeType == LayoutTimeSizeType.Star) - { - ++starCount; - def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" - if (def.UserSize.Value > maxStar) - { - maxStar = def.UserSize.Value; - } - } - } - - if (double.IsPositiveInfinity(maxStar)) - { - // negative scale means one or more of the weights was Infinity - scale = -1.0; - } - else if (starCount > 0) - { - // if maxStar * starCount > double.Max, summing all the weights could cause - // floating-point overflow. To avoid that, scale the weights by a factor to keep - // the sum within limits. Choose a power of 2, to preserve precision. - double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); - if (power < 0.0) - { - scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia - } - } - - // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights - // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. - // More explanation below... - for (bool runPhase2and3 = true; runPhase2and3;) - { - // Phase 2. Compute total *-weight W and available space S. - // For *-items that have Min or Max constraints, compute the ratios used to decide - // whether proportional space is too big or too small and add the item to the - // corresponding list. (The "min" list is in the first half of tempDefinitions, - // the "max" list in the second half. TempDefinitions has capacity at least - // 2*defCount, so there's room for both lists.) - totalStarWeight = 0.0; - takenSize = 0.0; - minCount = maxCount = 0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - switch (def.SizeType) - { - case (LayoutTimeSizeType.Auto): - takenSize += definitions[i].MinSize; - break; - case (LayoutTimeSizeType.Pixel): - takenSize += def.MeasureSize; - break; - case (LayoutTimeSizeType.Star): - if (def.MeasureSize < 0.0) - { - takenSize += -def.MeasureSize; // already resolved - } - else - { - double starWeight = StarWeight(def, scale); - totalStarWeight += starWeight; - - if (def.MinSize > 0.0) - { - // store ratio w/min in MeasureSize (for now) - tempDefinitions[minCount++] = def; - def.MeasureSize = starWeight / def.MinSize; - } - - double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); - if (!double.IsPositiveInfinity(effectiveMaxSize)) - { - // store ratio w/max in SizeCache (for now) - tempDefinitions[defCount + maxCount++] = def; - def.SizeCache = starWeight / effectiveMaxSize; - } - } - break; - } - } - - // Phase 3. Resolve *-items whose proportional sizes are too big or too small. - int minCountPhase2 = minCount, maxCountPhase2 = maxCount; - double takenStarWeight = 0.0; - double remainingAvailableSize = availableSize - takenSize; - double remainingStarWeight = totalStarWeight - takenStarWeight; - Array.Sort(tempDefinitions, 0, minCount, _minRatioComparer); - Array.Sort(tempDefinitions, defCount, maxCount, _maxRatioComparer); - - while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) - { - // the calculation - // remainingStarWeight = totalStarWeight - takenStarWeight - // is subject to catastrophic cancellation if the two terms are nearly equal, - // which leads to meaningless results. Check for that, and recompute from - // the remaining definitions. [This leads to quadratic behavior in really - // pathological cases - but they'd never arise in practice.] - const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate - if (remainingStarWeight < totalStarWeight * starFactor) - { - takenStarWeight = 0.0; - totalStarWeight = 0.0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - if (def.SizeType == LayoutTimeSizeType.Star && def.MeasureSize > 0.0) - { - totalStarWeight += StarWeight(def, scale); - } - } - - remainingStarWeight = totalStarWeight - takenStarWeight; - } - - double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : double.PositiveInfinity; - double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; - - // choose the def with larger ratio to the current proportion ("max discrepancy") - double proportion = remainingStarWeight / remainingAvailableSize; - bool? chooseMin = Choose(minRatio, maxRatio, proportion); - - // if no def was chosen, advance to phase 4; the current proportion doesn't - // conflict with any min or max values. - if (!(chooseMin.HasValue)) - { - break; - } - - // get the chosen definition and its resolved size - DefinitionBase resolvedDef; - double resolvedSize; - if (chooseMin == true) - { - resolvedDef = tempDefinitions[minCount - 1]; - resolvedSize = resolvedDef.MinSize; - --minCount; - } - else - { - resolvedDef = tempDefinitions[defCount + maxCount - 1]; - resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); - --maxCount; - } - - // resolve the chosen def, deduct its contributions from W and S. - // Defs resolved in phase 3 are marked by storing the negative of their resolved - // size in MeasureSize, to distinguish them from a pending def. - takenSize += resolvedSize; - resolvedDef.MeasureSize = -resolvedSize; - takenStarWeight += StarWeight(resolvedDef, scale); - --starCount; - - remainingAvailableSize = availableSize - takenSize; - remainingStarWeight = totalStarWeight - takenStarWeight; - - // advance to the next candidate defs, removing ones that have been resolved. - // Both counts are advanced, as a def might appear in both lists. - while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) - { - --minCount; - tempDefinitions[minCount] = null; - } - while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) - { - --maxCount; - tempDefinitions[defCount + maxCount] = null; - } - } - - // decide whether to run Phase2 and Phase3 again. There are 3 cases: - // 1. There is space available, and *-defs remaining. This is the - // normal case - move on to Phase 4 to allocate the remaining - // space proportionally to the remaining *-defs. - // 2. There is space available, but no *-defs. This implies at least one - // def was resolved as 'max', taking less space than its proportion. - // If there are also 'min' defs, reconsider them - we can give - // them more space. If not, all the *-defs are 'max', so there's - // no way to use all the available space. - // 3. We allocated too much space. This implies at least one def was - // resolved as 'min'. If there are also 'max' defs, reconsider - // them, otherwise the over-allocation is an inevitable consequence - // of the given min constraints. - // Note that if we return to Phase2, at least one *-def will have been - // resolved. This guarantees we don't run Phase2+3 infinitely often. - runPhase2and3 = false; - if (starCount == 0 && takenSize < availableSize) - { - // if no *-defs remain and we haven't allocated all the space, reconsider the defs - // resolved as 'min'. Their allocation can be increased to make up the gap. - for (int i = minCount; i < minCountPhase2; ++i) - { - DefinitionBase def = tempDefinitions[i]; - if (def != null) - { - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - - if (takenSize > availableSize) - { - // if we've allocated too much space, reconsider the defs - // resolved as 'max'. Their allocation can be decreased to make up the gap. - for (int i = maxCount; i < maxCountPhase2; ++i) - { - DefinitionBase def = tempDefinitions[defCount + i]; - if (def != null) - { - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - } - - // Phase 4. Resolve the remaining defs proportionally. - starCount = 0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.SizeType == LayoutTimeSizeType.Star) - { - if (def.MeasureSize < 0.0) - { - // this def was resolved in phase 3 - fix up its measure size - def.MeasureSize = -def.MeasureSize; - } - else - { - // this def needs resolution, add it to the list, sorted by *-weight - tempDefinitions[starCount++] = def; - def.MeasureSize = StarWeight(def, scale); - } - } - } - - if (starCount > 0) - { - Array.Sort(tempDefinitions, 0, starCount, _starWeightComparer); - - // compute the partial sums of *-weight, in increasing order of weight - // for minimal loss of precision. - totalStarWeight = 0.0; - for (int i = 0; i < starCount; ++i) - { - DefinitionBase def = tempDefinitions[i]; - totalStarWeight += def.MeasureSize; - def.SizeCache = totalStarWeight; - } - - // resolve the defs, in decreasing order of weight - for (int i = starCount - 1; i >= 0; --i) - { - DefinitionBase def = tempDefinitions[i]; - double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; - - // min and max should have no effect by now, but just in case... - resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); - resolvedSize = Math.Max(def.MinSize, resolvedSize); - - def.MeasureSize = resolvedSize; - takenSize += resolvedSize; - } - } - } - - /// - /// Calculates desired size for given array of definitions. - /// - /// Array of definitions to use for calculations. - /// Desired size. - private double CalculateDesiredSize( - DefinitionBase[] definitions) - { - double desiredSize = 0; - - for (int i = 0; i < definitions.Length; ++i) - { - desiredSize += definitions[i].MinSize; - } - - return (desiredSize); - } - - /// - /// Calculates and sets final size for all definitions in the given array. - /// - /// Array of definitions to process. - /// Final size to lay out to. - /// True if sizing row definitions, false for columns - private void SetFinalSize( - DefinitionBase[] definitions, - double finalSize, - bool columns) - { - int defCount = definitions.Length; - int[] definitionIndices = DefinitionIndices; - int minCount = 0, maxCount = 0; - double takenSize = 0.0; - double totalStarWeight = 0.0; - int starCount = 0; // number of unresolved *-definitions - double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" - - // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights - double maxStar = 0.0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.UserSize.IsStar) - { - ++starCount; - def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" - if (def.UserSize.Value > maxStar) - { - maxStar = def.UserSize.Value; - } - } - } - - if (double.IsPositiveInfinity(maxStar)) - { - // negative scale means one or more of the weights was Infinity - scale = -1.0; - } - else if (starCount > 0) - { - // if maxStar * starCount > double.Max, summing all the weights could cause - // floating-point overflow. To avoid that, scale the weights by a factor to keep - // the sum within limits. Choose a power of 2, to preserve precision. - double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); - if (power < 0.0) - { - scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia - } - } - - - // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights - // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. - // More explanation below... - for (bool runPhase2and3 = true; runPhase2and3;) - { - // Phase 2. Compute total *-weight W and available space S. - // For *-items that have Min or Max constraints, compute the ratios used to decide - // whether proportional space is too big or too small and add the item to the - // corresponding list. (The "min" list is in the first half of definitionIndices, - // the "max" list in the second half. DefinitionIndices has capacity at least - // 2*defCount, so there's room for both lists.) - totalStarWeight = 0.0; - takenSize = 0.0; - minCount = maxCount = 0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.UserSize.IsStar) - { - // Debug.Assert(!def.IsShared, "*-defs cannot be shared"); - - if (def.MeasureSize < 0.0) - { - takenSize += -def.MeasureSize; // already resolved - } - else - { - double starWeight = StarWeight(def, scale); - totalStarWeight += starWeight; - - if (def.MinSize > 0.0) - { - // store ratio w/min in MeasureSize (for now) - definitionIndices[minCount++] = i; - def.MeasureSize = starWeight / def.MinSize; - } - - double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); - if (!double.IsPositiveInfinity(effectiveMaxSize)) - { - // store ratio w/max in SizeCache (for now) - definitionIndices[defCount + maxCount++] = i; - def.SizeCache = starWeight / effectiveMaxSize; - } - } - } - else - { - double userSize = 0; - - switch (def.UserSize.GridUnitType) - { - case (GridUnitType.Pixel): - userSize = def.UserSize.Value; - break; - - case (GridUnitType.Auto): - userSize = def.MinSize; - break; - } - - double userMaxSize; - - // if (def.IsShared) - // { - // // overriding userMaxSize effectively prevents squishy-ness. - // // this is a "solution" to avoid shared definitions from been sized to - // // different final size at arrange time, if / when different grids receive - // // different final sizes. - // userMaxSize = userSize; - // } - // else - // { - userMaxSize = def.UserMaxSize; - // } - - def.SizeCache = Math.Max(def.MinSize, Math.Min(userSize, userMaxSize)); - takenSize += def.SizeCache; - } - } - - // Phase 3. Resolve *-items whose proportional sizes are too big or too small. - int minCountPhase2 = minCount, maxCountPhase2 = maxCount; - double takenStarWeight = 0.0; - double remainingAvailableSize = finalSize - takenSize; - double remainingStarWeight = totalStarWeight - takenStarWeight; - - MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); - Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); - MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); - Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); - - while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) - { - // the calculation - // remainingStarWeight = totalStarWeight - takenStarWeight - // is subject to catastrophic cancellation if the two terms are nearly equal, - // which leads to meaningless results. Check for that, and recompute from - // the remaining definitions. [This leads to quadratic behavior in really - // pathological cases - but they'd never arise in practice.] - const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate - if (remainingStarWeight < totalStarWeight * starFactor) - { - takenStarWeight = 0.0; - totalStarWeight = 0.0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - if (def.UserSize.IsStar && def.MeasureSize > 0.0) - { - totalStarWeight += StarWeight(def, scale); - } - } - - remainingStarWeight = totalStarWeight - takenStarWeight; - } - - double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : double.PositiveInfinity; - double maxRatio = (maxCount > 0) ? definitions[definitionIndices[defCount + maxCount - 1]].SizeCache : -1.0; - - // choose the def with larger ratio to the current proportion ("max discrepancy") - double proportion = remainingStarWeight / remainingAvailableSize; - bool? chooseMin = Choose(minRatio, maxRatio, proportion); - - // if no def was chosen, advance to phase 4; the current proportion doesn't - // conflict with any min or max values. - if (!(chooseMin.HasValue)) - { - break; - } - - // get the chosen definition and its resolved size - int resolvedIndex; - DefinitionBase resolvedDef; - double resolvedSize; - if (chooseMin == true) - { - resolvedIndex = definitionIndices[minCount - 1]; - resolvedDef = definitions[resolvedIndex]; - resolvedSize = resolvedDef.MinSize; - --minCount; - } - else - { - resolvedIndex = definitionIndices[defCount + maxCount - 1]; - resolvedDef = definitions[resolvedIndex]; - resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); - --maxCount; - } - - // resolve the chosen def, deduct its contributions from W and S. - // Defs resolved in phase 3 are marked by storing the negative of their resolved - // size in MeasureSize, to distinguish them from a pending def. - takenSize += resolvedSize; - resolvedDef.MeasureSize = -resolvedSize; - takenStarWeight += StarWeight(resolvedDef, scale); - --starCount; - - remainingAvailableSize = finalSize - takenSize; - remainingStarWeight = totalStarWeight - takenStarWeight; - - // advance to the next candidate defs, removing ones that have been resolved. - // Both counts are advanced, as a def might appear in both lists. - while (minCount > 0 && definitions[definitionIndices[minCount - 1]].MeasureSize < 0.0) - { - --minCount; - definitionIndices[minCount] = -1; - } - while (maxCount > 0 && definitions[definitionIndices[defCount + maxCount - 1]].MeasureSize < 0.0) - { - --maxCount; - definitionIndices[defCount + maxCount] = -1; - } - } - - // decide whether to run Phase2 and Phase3 again. There are 3 cases: - // 1. There is space available, and *-defs remaining. This is the - // normal case - move on to Phase 4 to allocate the remaining - // space proportionally to the remaining *-defs. - // 2. There is space available, but no *-defs. This implies at least one - // def was resolved as 'max', taking less space than its proportion. - // If there are also 'min' defs, reconsider them - we can give - // them more space. If not, all the *-defs are 'max', so there's - // no way to use all the available space. - // 3. We allocated too much space. This implies at least one def was - // resolved as 'min'. If there are also 'max' defs, reconsider - // them, otherwise the over-allocation is an inevitable consequence - // of the given min constraints. - // Note that if we return to Phase2, at least one *-def will have been - // resolved. This guarantees we don't run Phase2+3 infinitely often. - runPhase2and3 = false; - if (starCount == 0 && takenSize < finalSize) - { - // if no *-defs remain and we haven't allocated all the space, reconsider the defs - // resolved as 'min'. Their allocation can be increased to make up the gap. - for (int i = minCount; i < minCountPhase2; ++i) - { - if (definitionIndices[i] >= 0) - { - DefinitionBase def = definitions[definitionIndices[i]]; - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - - if (takenSize > finalSize) - { - // if we've allocated too much space, reconsider the defs - // resolved as 'max'. Their allocation can be decreased to make up the gap. - for (int i = maxCount; i < maxCountPhase2; ++i) - { - if (definitionIndices[defCount + i] >= 0) - { - DefinitionBase def = definitions[definitionIndices[defCount + i]]; - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - } - - // Phase 4. Resolve the remaining defs proportionally. - starCount = 0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.UserSize.IsStar) - { - if (def.MeasureSize < 0.0) - { - // this def was resolved in phase 3 - fix up its size - def.SizeCache = -def.MeasureSize; - } - else - { - // this def needs resolution, add it to the list, sorted by *-weight - definitionIndices[starCount++] = i; - def.MeasureSize = StarWeight(def, scale); - } - } - } - - if (starCount > 0) - { - StarWeightIndexComparer starWeightIndexComparer = new StarWeightIndexComparer(definitions); - Array.Sort(definitionIndices, 0, starCount, starWeightIndexComparer); - - // compute the partial sums of *-weight, in increasing order of weight - // for minimal loss of precision. - totalStarWeight = 0.0; - for (int i = 0; i < starCount; ++i) - { - DefinitionBase def = definitions[definitionIndices[i]]; - totalStarWeight += def.MeasureSize; - def.SizeCache = totalStarWeight; - } - - // resolve the defs, in decreasing order of weight. - for (int i = starCount - 1; i >= 0; --i) - { - DefinitionBase def = definitions[definitionIndices[i]]; - double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(finalSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; - - // min and max should have no effect by now, but just in case... - resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); - resolvedSize = Math.Max(def.MinSize, resolvedSize); - - // Use the raw (unrounded) sizes to update takenSize, so that - // proportions are computed in the same terms as in phase 3; - // this avoids errors arising from min/max constraints. - takenSize += resolvedSize; - def.SizeCache = resolvedSize; - } - } - - // Phase 5. Apply layout rounding. We do this after fully allocating - // unrounded sizes, to avoid breaking assumptions in the previous phases - if (UseLayoutRounding) - { - var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; - - double[] roundingErrors = RoundingErrors; - double roundedTakenSize = 0.0; - - // round each of the allocated sizes, keeping track of the deltas - for (int i = 0; i < definitions.Length; ++i) - { - DefinitionBase def = definitions[i]; - double roundedSize = RoundLayoutValue(def.SizeCache, dpi); - roundingErrors[i] = (roundedSize - def.SizeCache); - def.SizeCache = roundedSize; - roundedTakenSize += roundedSize; - } - - // The total allocation might differ from finalSize due to rounding - // effects. Tweak the allocations accordingly. - - // Theoretical and historical note. The problem at hand - allocating - // space to columns (or rows) with *-weights, min and max constraints, - // and layout rounding - has a long history. Especially the special - // case of 50 columns with min=1 and available space=435 - allocating - // seats in the U.S. House of Representatives to the 50 states in - // proportion to their population. There are numerous algorithms - // and papers dating back to the 1700's, including the book: - // Balinski, M. and H. Young, Fair Representation, Yale University Press, New Haven, 1982. - // - // One surprising result of all this research is that *any* algorithm - // will suffer from one or more undesirable features such as the - // "population paradox" or the "Alabama paradox", where (to use our terminology) - // increasing the available space by one pixel might actually decrease - // the space allocated to a given column, or increasing the weight of - // a column might decrease its allocation. This is worth knowing - // in case someone complains about this behavior; it's not a bug so - // much as something inherent to the problem. Cite the book mentioned - // above or one of the 100s of references, and resolve as WontFix. - // - // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) - // each being allocated a large number of pixels (~50 or greater), and - // people don't even notice the kind of 1-pixel anomolies that are - // theoretically inevitable, or don't care if they do. At least they shouldn't - // care - no one should be using the results WPF's grid layout to make - // quantitative decisions; its job is to produce a reasonable display, not - // to allocate seats in Congress. - // - // Our algorithm is more susceptible to paradox than the one currently - // used for Congressional allocation ("Huntington-Hill" algorithm), but - // it is faster to run: O(N log N) vs. O(S * N), where N=number of - // definitions, S = number of available pixels. And it produces - // adequate results in practice, as mentioned above. - // - // To reiterate one point: all this only applies when layout rounding - // is in effect. When fractional sizes are allowed, the algorithm - // behaves as well as possible, subject to the min/max constraints - // and precision of floating-point computation. (However, the resulting - // display is subject to anti-aliasing problems. TANSTAAFL.) - - if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) - { - // Compute deltas - for (int i = 0; i < definitions.Length; ++i) - { - definitionIndices[i] = i; - } - - // Sort rounding errors - RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); - Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); - double adjustedSize = roundedTakenSize; - double dpiIncrement = 1.0 / dpi; - - if (roundedTakenSize > finalSize) - { - int i = definitions.Length - 1; - while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache - dpiIncrement; - final = Math.Max(final, definition.MinSize); - if (final < definition.SizeCache) - { - adjustedSize -= dpiIncrement; - } - definition.SizeCache = final; - i--; - } - } - else if (roundedTakenSize < finalSize) - { - int i = 0; - while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Length) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache + dpiIncrement; - final = Math.Max(final, definition.MinSize); - if (final > definition.SizeCache) - { - adjustedSize += dpiIncrement; - } - definition.SizeCache = final; - i++; - } - } - } - } - - // Phase 6. Compute final offsets - definitions[0].FinalOffset = 0.0; - for (int i = 0; i < definitions.Length; ++i) - { - definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; - } - } - - // Choose the ratio with maximum discrepancy from the current proportion. - // Returns: - // true if proportion fails a min constraint but not a max, or - // if the min constraint has higher discrepancy - // false if proportion fails a max constraint but not a min, or - // if the max constraint has higher discrepancy - // null if proportion doesn't fail a min or max constraint - // The discrepancy is the ratio of the proportion to the max- or min-ratio. - // When both ratios hit the constraint, minRatio < proportion < maxRatio, - // and the minRatio has higher discrepancy if - // (proportion / minRatio) > (maxRatio / proportion) - private static bool? Choose(double minRatio, double maxRatio, double proportion) - { - if (minRatio < proportion) - { - if (maxRatio > proportion) - { - // compare proportion/minRatio : maxRatio/proportion, but - // do it carefully to avoid floating-point overflow or underflow - // and divide-by-0. - double minPower = Math.Floor(Math.Log(minRatio, 2.0)); - double maxPower = Math.Floor(Math.Log(maxRatio, 2.0)); - double f = Math.Pow(2.0, Math.Floor((minPower + maxPower) / 2.0)); - if ((proportion / f) * (proportion / f) > (minRatio / f) * (maxRatio / f)) - { - return true; - } - else - { - return false; - } - } - else - { - return true; - } - } - else if (maxRatio > proportion) - { - return false; - } - - return null; - } - - /// - /// Sorts row/column indices by rounding error if layout rounding is applied. - /// - /// Index, rounding error pair - /// Index, rounding error pair - /// 1 if x.Value > y.Value, 0 if equal, -1 otherwise - private static int CompareRoundingErrors(KeyValuePair x, KeyValuePair y) - { - if (x.Value < y.Value) - { - return -1; - } - else if (x.Value > y.Value) - { - return 1; - } - return 0; - } - - /// - /// Calculates final (aka arrange) size for given range. - /// - /// Array of definitions to process. - /// Start of the range. - /// Number of items in the range. - /// Final size. - private double GetFinalSizeForRange( - DefinitionBase[] definitions, - int start, - int count) - { - double size = 0; - int i = start + count - 1; - - do - { - size += definitions[i].SizeCache; - } while (--i >= start); - - return (size); - } - - /// - /// Clears dirty state for the grid and its columns / rows - /// - private void SetValid() - { - if (IsTrivialGrid) - { - if (_tempDefinitions != null) - { - // TempDefinitions has to be cleared to avoid "memory leaks" - Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); - _tempDefinitions = null; - } - } - } - - - /// - /// Synchronized ShowGridLines property with the state of the grid's visual collection - /// by adding / removing GridLinesRenderer visual. - /// Returns a reference to GridLinesRenderer visual or null. - /// - private GridLinesRenderer EnsureGridLinesRenderer() - { - // - // synchronize the state - // - if (ShowGridLines && (_gridLinesRenderer == null)) - { - _gridLinesRenderer = new GridLinesRenderer(); - this.VisualChildren.Add(_gridLinesRenderer); - } - - if ((!ShowGridLines) && (_gridLinesRenderer != null)) - { - this.VisualChildren.Remove(_gridLinesRenderer); - _gridLinesRenderer = null; - } - - return (_gridLinesRenderer); - } - - private double RoundLayoutValue(double value, double dpiScale) - { - double newValue; - - // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.AreClose(dpiScale, 1.0)) - { - newValue = Math.Round(value * dpiScale) / dpiScale; - // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. - if (double.IsNaN(newValue) || - double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, double.MaxValue)) - { - newValue = value; - } - } - else - { - newValue = Math.Round(value); - } - - return newValue; - } - - - private static int ValidateColumn(AvaloniaObject o, int value) - { - if (value < 0) - { - throw new ArgumentException("Invalid Grid.Column value."); - } - - return value; - } - - private static int ValidateRow(AvaloniaObject o, int value) - { - if (value < 0) - { - throw new ArgumentException("Invalid Grid.Row value."); - } - - return value; - } - - private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) - { - if (!grid.IsTrivialGrid) // trivial grid is 1 by 1. there is no grid lines anyway - { - grid.Invalidate(); - } - } - - /// - /// Helper for Comparer methods. - /// - /// - /// true if one or both of x and y are null, in which case result holds - /// the relative sort order. - /// - private static bool CompareNullRefs(object x, object y, out int result) - { - result = 2; - - if (x == null) - { - if (y == null) - { - result = 0; - } - else - { - result = -1; - } - } - else - { - if (y == null) - { - result = 1; - } - } - - return (result != 2); - } - - /// - /// Helper accessor to layout time array of definitions. - /// - private DefinitionBase[] TempDefinitions - { - get - { - int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; - - if (_tempDefinitions == null - || _tempDefinitions.Length < requiredLength) - { - WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); - if (tempDefinitionsWeakRef == null) - { - _tempDefinitions = new DefinitionBase[requiredLength]; - Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); - } - else - { - _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; - if (_tempDefinitions == null - || _tempDefinitions.Length < requiredLength) - { - _tempDefinitions = new DefinitionBase[requiredLength]; - tempDefinitionsWeakRef.Target = _tempDefinitions; - } - } - } - return (_tempDefinitions); - } - } - - /// - /// Helper accessor to definition indices. - /// - private int[] DefinitionIndices - { - get - { - int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; - - if (_definitionIndices == null || _definitionIndices.Length < requiredLength) - { - _definitionIndices = new int[requiredLength]; - } - - return _definitionIndices; - } - } - - /// - /// Helper accessor to rounding errors. - /// - private double[] RoundingErrors - { - get - { - int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); - - if (_roundingErrors == null && requiredLength == 0) - { - _roundingErrors = new double[1]; - } - else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) - { - _roundingErrors = new double[requiredLength]; - } - return _roundingErrors; - } - } - - /// - /// Returns *-weight, adjusted for scale computed during Phase 1 - /// - static double StarWeight(DefinitionBase def, double scale) - { - if (scale < 0.0) - { - // if one of the *-weights is Infinity, adjust the weights by mapping - // Infinty to 1.0 and everything else to 0.0: the infinite items share the - // available space equally, everyone else gets nothing. - return (double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; - } - else - { - return def.UserSize.Value * scale; - } - } - - /// - /// LayoutTimeSizeType is used internally and reflects layout-time size type. - /// - [System.Flags] - internal enum LayoutTimeSizeType : byte - { - None = 0x00, - Pixel = 0x01, - Auto = 0x02, - Star = 0x04, - } - - /// - /// CellCache stored calculated values of - /// 1. attached cell positioning properties; - /// 2. size type; - /// 3. index of a next cell in the group; - /// - private struct CellCache - { - internal int ColumnIndex; - internal int RowIndex; - internal int ColumnSpan; - internal int RowSpan; - internal LayoutTimeSizeType SizeTypeU; - internal LayoutTimeSizeType SizeTypeV; - internal int Next; - internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } - internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } - } - - /// - /// Helper class for representing a key for a span in hashtable. - /// - private class SpanKey - { - /// - /// Constructor. - /// - /// Starting index of the span. - /// Span count. - /// true for columns; false for rows. - internal SpanKey(int start, int count, bool u) - { - _start = start; - _count = count; - _u = u; - } - - /// - /// - /// - public override int GetHashCode() - { - int hash = (_start ^ (_count << 2)); - - if (_u) hash &= 0x7ffffff; - else hash |= 0x8000000; - - return (hash); - } - - /// - /// - /// - public override bool Equals(object obj) - { - SpanKey sk = obj as SpanKey; - return (sk != null - && sk._start == _start - && sk._count == _count - && sk._u == _u); - } - - /// - /// Returns start index of the span. - /// - internal int Start { get { return (_start); } } - - /// - /// Returns span count. - /// - internal int Count { get { return (_count); } } - - /// - /// Returns true if this is a column span. - /// false if this is a row span. - /// - internal bool U { get { return (_u); } } - - private int _start; - private int _count; - private bool _u; - } - - /// - /// SpanPreferredDistributionOrderComparer. - /// - private class SpanPreferredDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - if (definitionX.UserSize.IsAuto) - { - if (definitionY.UserSize.IsAuto) - { - result = definitionX.MinSize.CompareTo(definitionY.MinSize); - } - else - { - result = -1; - } - } - else - { - if (definitionY.UserSize.IsAuto) - { - result = +1; - } - else - { - result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); - } - } - } - - return result; - } - } - - /// - /// SpanMaxDistributionOrderComparer. - /// - private class SpanMaxDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - if (definitionX.UserSize.IsAuto) - { - if (definitionY.UserSize.IsAuto) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - else - { - result = +1; - } - } - else - { - if (definitionY.UserSize.IsAuto) - { - result = -1; - } - else - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - } - } - - return result; - } - } - - /// - /// RoundingErrorIndexComparer. - /// - private class RoundingErrorIndexComparer : IComparer - { - private readonly double[] errors; - - internal RoundingErrorIndexComparer(double[] errors) - { - Contract.Requires(errors != null); - this.errors = errors; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - int result; - - if (!CompareNullRefs(indexX, indexY, out result)) - { - double errorX = errors[indexX.Value]; - double errorY = errors[indexY.Value]; - result = errorX.CompareTo(errorY); - } - - return result; - } - } - - /// - /// MinRatioComparer. - /// Sort by w/min (stored in MeasureSize), descending. - /// We query the list from the back, i.e. in ascending order of w/min. - /// - private class MinRatioComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionY, definitionX, out result)) - { - result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); - } - - return result; - } - } - - /// - /// MaxRatioComparer. - /// Sort by w/max (stored in SizeCache), ascending. - /// We query the list from the back, i.e. in descending order of w/max. - /// - private class MaxRatioComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// StarWeightComparer. - /// Sort by *-weight (stored in MeasureSize), ascending. - /// - private class StarWeightComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); - } - - return result; - } - } - - /// - /// MinRatioIndexComparer. - /// - private class MinRatioIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal MinRatioIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionY, definitionX, out result)) - { - result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); - } - - return result; - } - } - - /// - /// MaxRatioIndexComparer. - /// - private class MaxRatioIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal MaxRatioIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// MaxRatioIndexComparer. - /// - private class StarWeightIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal StarWeightIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); - } - - return result; - } - } - - /// - /// Helper to render grid lines. - /// - private class GridLinesRenderer : Control - { - /// - /// Static initialization - /// - static GridLinesRenderer() - { - var oddDashArray = new List(); - oddDashArray.Add(c_dashLength); - oddDashArray.Add(c_dashLength); - var ds1 = new DashStyle(oddDashArray, 0); - s_oddDashPen = new Pen(Brushes.Blue, - c_penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds1); - - var evenDashArray = new List(); - evenDashArray.Add(c_dashLength); - evenDashArray.Add(c_dashLength); - var ds2 = new DashStyle(evenDashArray, 0); - s_evenDashPen = new Pen(Brushes.Yellow, - c_penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds2); - } - - /// - /// UpdateRenderBounds. - /// - public override void Render(DrawingContext drawingContext) - { - var grid = this.GetVisualParent(); - - if (grid == null - || !grid.ShowGridLines - || grid.IsTrivialGrid) - { - return; - } - - for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) - { - DrawGridLine( - drawingContext, - grid.ColumnDefinitions[i].FinalOffset, 0.0, - grid.ColumnDefinitions[i].FinalOffset, lastArrangeSize.Height); - } - - for (int i = 1; i < grid.RowDefinitions.Count; ++i) - { - DrawGridLine( - drawingContext, - 0.0, grid.RowDefinitions[i].FinalOffset, - lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); - } - } - - /// - /// Draw single hi-contrast line. - /// - private static void DrawGridLine( - DrawingContext drawingContext, - double startX, - double startY, - double endX, - double endY) - { - var start = new Point(startX, startY); - var end = new Point(endX, endY); - drawingContext.DrawLine(s_oddDashPen, start, end); - drawingContext.DrawLine(s_evenDashPen, start, end); - } - - internal void UpdateRenderBounds(Size arrangeSize) - { - lastArrangeSize = arrangeSize; - this.InvalidateVisual(); - } - - private static Size lastArrangeSize; - private const double c_dashLength = 4.0; // - private const double c_penWidth = 1.0; // - private static readonly Pen s_oddDashPen; // first pen to draw dash - private static readonly Pen s_evenDashPen; // second pen to draw dash - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs deleted file mode 100644 index 7704228a4e..0000000000 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ /dev/null @@ -1,705 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Layout; -using JetBrains.Annotations; - -namespace Avalonia.Controls.Utils -{ - /// - /// 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. - /// - /// - /// Overriding conventions that allows the algorithm to handle external inputa - /// - /// - /// The measured result that containing the desired size and all the column/row lengths. - /// - [NotNull, Pure] - internal MeasureResult Measure(double containerLength, IReadOnlyList conventions = null) - { - // Prepare all the variables that this method needs to use. - conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); - var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - var aggregatedLength = 0.0; - double starUnitLength; - - // M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`. - // We mark the aggregated length as "fix" because we can completely determine their values. Same as below. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // |#fix#| |#fix#| - // - // 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。 - // 我们会将所有能够确定下长度的行列标记为 fix。下同。 - // 请注意: - // - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。 - - aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // M3/7. Fix all the * lengths that have reached the minimum. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // | fix | |#fix#| fix | - - var shouldTestStarMin = true; - while (shouldTestStarMin) - { - // Calculate the unit * length to estimate the length of each column/row that has * length. - // Under this estimated length, check if there is a minimum value that has a length less than its constraint. - // If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value. - // - // 计算单位 * 的长度,以便预估出每一个 * 行列的长度。 - // 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。 - // 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。 - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - foreach (var convention in conventions.Where(x => x.Length.IsStar)) - { - var (star, min) = (convention.Length.Value, convention.MinLength); - var starLength = star * starUnitLength; - if (starLength < min) - { - convention.Fix(min); - starLength = min; - aggregatedLength += starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMin = @fixed; - } - - // M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| | fix |#fix#| fix | fix | - - var shouldTestAuto = true; - while (shouldTestAuto) - { - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - for (var i = 0; i < conventions.Count; i++) - { - var convention = conventions[i]; - if (!convention.Length.IsAuto) - { - continue; - } - - var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); - convention.Fix(more); - aggregatedLength += more; - @fixed = true; - break; - } - - shouldTestAuto = @fixed; - } - - // M5/7. Expand the stars according to the additional conventions (usually the child desired length). - // We can't fix this kind of length, so we just mark them as desired (des). - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - - var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); - aggregatedLength += desiredStarMin; - - // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. - // Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - // Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure. - // - // desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des) - // greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des - - var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; - var greedyDesiredLength = aggregatedLength; - - // M7/7. Expand all the rest stars. These stars have no conventions or only have - // max value they can be expanded from zero to constraint. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#| - // Note: This table will be stored as the final result into the MeasureResult. - - var dynamicConvention = ExpandStars(conventions, containerLength); - Clip(dynamicConvention, containerLength); - - // Returns the measuring result. - return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, - conventions, dynamicConvention, minLengths); - } - - /// - /// 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, measure.LeanLengthList); - } - else if (finalLength - measure.ContainerLength < -LayoutTolerance) - { - // If the final length is smaller, we measure the M6/6 procedure only. - var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); - measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, - measure.LeanLengthList, dynamicConvention, measure.MinLengths); - } - - return new ArrangeResult(measure.LengthList); - } - - /// - /// 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 (List, 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 maximum desired size. - - // Before we determine the behavior of this method, we just aggregate the one-span * columns. - - var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // Prepare a lengthList variable indicating the fixed length of each column/row. - var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); - foreach (var group in _additionalConventions - .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) - .ToLookup(x => x.Index)) - { - lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); - } - - // Now the lengthList is fixed by every one-span columns/rows. - // Then we should determine the multi-span column's/row's length. - foreach (var group in _additionalConventions - .Where(x => x.Span > 1) - .ToLookup(x => x.Index + x.Span - 1) - // Order the multi-span columns/rows by last index. - .OrderBy(x => x.Key)) - { - var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); - lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); - } - - return (lengthList, lengthList.Sum() - fixedLength); - } - - /// - /// 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, IReadOnlyList minLengths) - { - ContainerLength = containerLength; - DesiredLength = desiredLength; - GreedyDesiredLength = greedyDesiredLength; - LeanLengthList = leanConventions; - LengthList = expandedConventions; - MinLengths = minLengths; - } - - /// - /// 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; } - public IReadOnlyList MinLengths { 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.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs deleted file mode 100644 index 5f70557385..0000000000 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ /dev/null @@ -1,651 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.Layout; -using Avalonia.VisualTree; - -namespace Avalonia.Controls -{ - /// - /// Shared size scope implementation. - /// Shares the size information between participating grids. - /// An instance of this class is attached to every that has its - /// IsSharedSizeScope property set to true. - /// - internal sealed class SharedSizeScopeHost : IDisposable - { - private enum MeasurementState - { - Invalidated, - Measuring, - Cached - } - - /// - /// Class containing the measured rows/columns for a single grid. - /// Monitors changes to the row/column collections as well as the SharedSizeGroup changes - /// for the individual items in those collections. - /// Notifies the of SharedSizeGroup changes. - /// - private sealed class MeasurementCache : IDisposable - { - readonly CompositeDisposable _subscriptions; - readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); - - public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; - - public MeasurementCache(Grid grid) - { - Grid = grid; - Results = grid.RowDefinitions.Cast() - .Concat(grid.ColumnDefinitions) - .Select(d => new MeasurementResult(grid, d)) - .ToList(); - - grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; - grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; - - - _subscriptions = new CompositeDisposable( - Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), - grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); - - } - - // method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid - private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - // route to collection changed as a Reset. - DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - private void DefinitionPropertyChanged(Tuple propertyChanged) - { - if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) - { - var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); - var oldName = result.SizeGroup?.Name; - var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; - _groupChanged.OnNext((oldName, newName, result)); - } - } - - private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - int offset = 0; - if (sender is ColumnDefinitions) - offset = Grid.RowDefinitions.Count; - - var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List(); - var oldItems = e.OldStartingIndex >= 0 - ? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) - : new List(); - - void NotifyNewItems() - { - foreach (var item in newItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); - } - } - - void NotifyOldItems() - { - foreach (var item in oldItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); - } - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Results.InsertRange(e.NewStartingIndex + offset, newItems); - NotifyNewItems(); - break; - - case NotifyCollectionChangedAction.Remove: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - NotifyOldItems(); - break; - - case NotifyCollectionChangedAction.Move: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, oldItems); - break; - - case NotifyCollectionChangedAction.Replace: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, newItems); - - NotifyOldItems(); - NotifyNewItems(); - - break; - - case NotifyCollectionChangedAction.Reset: - oldItems = Results; - newItems = Results = Grid.RowDefinitions.Cast() - .Concat(Grid.ColumnDefinitions) - .Select(d => new MeasurementResult(Grid, d)) - .ToList(); - NotifyOldItems(); - NotifyNewItems(); - - break; - } - } - - - /// - /// Updates the Results collection with Grid Measure results. - /// - /// Result of the GridLayout.Measure method for the RowDefinitions in the grid. - /// Result of the GridLayout.Measure method for the ColumnDefinitions in the grid. - public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - MeasurementState = MeasurementState.Cached; - for (int i = 0; i < Grid.RowDefinitions.Count; i++) - { - Results[i].MeasuredResult = rowResult.LengthList[i]; - Results[i].MinLength = rowResult.MinLengths[i]; - } - - for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) - { - Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; - Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; - } - } - - /// - /// Clears the measurement cache, in preparation for the Measure pass. - /// - public void InvalidateMeasure() - { - var newItems = new List(); - var oldItems = new List(); - - MeasurementState = MeasurementState.Invalidated; - - Results.ForEach(r => - { - r.MeasuredResult = double.NaN; - r.SizeGroup?.Reset(); - }); - } - - /// - /// Clears the subscriptions. - /// - public void Dispose() - { - _subscriptions.Dispose(); - _groupChanged.OnCompleted(); - } - - /// - /// Gets the for which this cache has been created. - /// - public Grid Grid { get; } - - /// - /// Gets the of this cache. - /// - public MeasurementState MeasurementState { get; private set; } - - /// - /// Gets the list of instances. - /// - /// - /// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions - /// - public List Results { get; private set; } - } - - - /// - /// Class containing the Measure result for a single Row/Column in a grid. - /// - private class MeasurementResult - { - public MeasurementResult(Grid owningGrid, DefinitionBase definition) - { - OwningGrid = owningGrid; - Definition = definition; - MeasuredResult = double.NaN; - } - - /// - /// Gets the / related to this - /// - public DefinitionBase Definition { get; } - - /// - /// Gets or sets the actual result of the Measure operation for this column. - /// - public double MeasuredResult { get; set; } - - /// - /// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids. - /// - public double MinLength { get; set; } - - /// - /// Gets or sets the that this result belongs to. - /// - public Group SizeGroup { get; set; } - - /// - /// Gets the Grid that is the parent of the Row/Column - /// - public Grid OwningGrid { get; } - - /// - /// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup. - /// - /// A tuple of length and the priority in the shared size group. - public (double length, int priority) GetPriorityLength() - { - var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; - - if (length.IsAbsolute) - return (MeasuredResult, 1); - if (length.IsAuto) - return (MeasuredResult, 2); - if (MinLength > 0) - return (MinLength, 3); - return (MeasuredResult, 4); - } - } - - /// - /// Visitor class used to gather the final length for a given SharedSizeGroup. - /// - /// - /// The values are applied according to priorities defined in . - /// - private class LentgthGatherer - { - /// - /// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup - /// - public double Length { get; private set; } - private int gatheredPriority = 6; - - /// - /// Visits the applying the result of to its internal cache. - /// - /// The instance to visit. - public void Visit(MeasurementResult result) - { - var (length, priority) = result.GetPriorityLength(); - - if (gatheredPriority < priority) - return; - - gatheredPriority = priority; - if (gatheredPriority == priority) - { - Length = Math.Max(length,Length); - } - else - { - Length = length; - } - } - } - - /// - /// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value. - /// - private class Group - { - private double? cachedResult; - private List _results = new List(); - - /// - /// Gets the name of the SharedSizeGroup. - /// - public string Name { get; } - - public Group(string name) - { - Name = name; - } - - /// - /// Gets the collection of the instances. - /// - public IReadOnlyList Results => _results; - - /// - /// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup. - /// - public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; - - /// - /// Clears the previously cached result in preparation for measurement. - /// - public void Reset() - { - cachedResult = null; - } - - /// - /// Ads a measurement result to this group and sets it's property - /// to this instance. - /// - /// The to include in this group. - public void Add(MeasurementResult result) - { - if (_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); - - result.SizeGroup = this; - _results.Add(result); - } - - /// - /// Removes the measurement result from this group and clears its value. - /// - /// The to clear. - public void Remove(MeasurementResult result) - { - if (!_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); - result.SizeGroup = null; - _results.Remove(result); - } - - - private double Gather() - { - var visitor = new LentgthGatherer(); - - _results.ForEach(visitor.Visit); - - return visitor.Length; - } - } - - private readonly AvaloniaList _measurementCaches = new AvaloniaList(); - private readonly Dictionary _groups = new Dictionary(); - private bool _invalidating; - - /// - /// Removes the SharedSizeScope and notifies all affected grids of the change. - /// - public void Dispose() - { - // while (_measurementCaches.Any()) - // _measurementCaches[0].Grid.SharedScopeChanged(); - } - - /// - /// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree. - /// - /// The to add to this scope. - internal void RegisterGrid(Grid toAdd) - { - if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); - - var cache = new MeasurementCache(toAdd); - _measurementCaches.Add(cache); - AddGridToScopes(cache); - } - - /// - /// Removes the registration for a grid in this SharedSizeScope. - /// - /// The to remove. - internal void UnegisterGrid(Grid toRemove) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); - - _measurementCaches.Remove(cache); - RemoveGridFromScopes(cache); - cache.Dispose(); - } - - /// - /// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope. - /// - /// The that should be checked. - /// True if the grid should forward its calls. - internal bool ParticipatesInScope(Grid toCheck) - { - return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) - ?.Results.Any(r => r.SizeGroup != null) ?? false; - } - - /// - /// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated. - /// Forwards the same call to all affected grids in this scope. - /// - /// The that had it's Measure invalidated. - internal void InvalidateMeasure(Grid grid) - { - // prevent stack overflow - if (_invalidating) - return; - _invalidating = true; - - InvalidateMeasureImpl(grid); - - _invalidating = false; - } - - /// - /// Updates the measurement cache with the results of the measurement pass. - /// - /// The that has been measured. - /// Measurement result for the grid's - /// Measurement result for the grid's - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); - - cache.UpdateMeasureResult(rowResult, columnResult); - } - - /// - /// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid. - /// - /// The that is being Arranged - /// The 's cached measurement result. - /// The 's cached measurement result. - /// Row and column measurement result updated with the SharedSizeScope constraints. - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - return ( - Arrange(grid.RowDefinitions, rowResult), - Arrange(grid.ColumnDefinitions, columnResult) - ); - } - - /// - /// Invalidates the measure of all grids affected by the SharedSizeGroups contained within. - /// - /// The that is being invalidated. - private void InvalidateMeasureImpl(Grid grid) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - - if (cache == null) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); - - // already invalidated the cache, early out. - if (cache.MeasurementState == MeasurementState.Invalidated) - return; - - // we won't calculate, so we should not invalidate. - if (!ParticipatesInScope(grid)) - return; - - cache.InvalidateMeasure(); - - // maybe there is a condition to only call arrange on some of the calls? - grid.InvalidateMeasure(); - - // find all the scopes within the invalidated grid - var scopeNames = cache.Results - .Where(mr => mr.SizeGroup != null) - .Select(mr => mr.SizeGroup.Name) - .Distinct(); - // find all grids related to those scopes - var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) - .Select(r => r.OwningGrid) - .Where(g => g.IsMeasureValid) - .Distinct(); - - // invalidate them as well - foreach (var otherGrid in otherGrids) - { - InvalidateMeasureImpl(otherGrid); - } - } - - /// - /// callback notifying the scope that a has changed its - /// SharedSizeGroup - /// - /// Old and New name (either can be null) of the SharedSizeGroup, as well as the result. - private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) - { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); - } - - /// - /// Handles the impact of SharedSizeGroups on the Arrange of / - /// - /// Rows/Columns that were measured - /// The initial measurement result. - /// Modified measure result - private GridLayout.MeasureResult Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) - { - var conventions = measureResult.LeanLengthList.ToList(); - var lengths = measureResult.LengthList.ToList(); - var desiredLength = 0.0; - for (int i = 0; i < definitions.Count; i++) - { - var definition = definitions[i]; - - // for empty SharedSizeGroups pass on unmodified result. - if (string.IsNullOrEmpty(definition.SharedSizeGroup)) - { - desiredLength += measureResult.LengthList[i]; - continue; - } - - var group = _groups[definition.SharedSizeGroup]; - // Length calculated over all Definitions participating in a SharedSizeGroup. - var length = group.CalculatedLength; - - conventions[i] = new GridLayout.LengthConvention( - new GridLength(length), - measureResult.LeanLengthList[i].MinLength, - measureResult.LeanLengthList[i].MaxLength - ); - lengths[i] = length; - desiredLength += length; - } - - return new GridLayout.MeasureResult( - measureResult.ContainerLength, - desiredLength, - measureResult.GreedyDesiredLength,//?? - conventions, - lengths, - measureResult.MinLengths); - } - - /// - /// Adds all measurement results for a grid to their repsective scopes. - /// - /// The for a grid to be added. - private void AddGridToScopes(MeasurementCache cache) - { - cache.GroupChanged.Subscribe(SharedGroupChanged); - - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - AddToGroup(scopeName, result); - } - } - - /// - /// Handles adding the to a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to add the to. - /// The to add to a scope. - private void AddToGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group(scopeName)); - - group.Add(result); - } - - /// - /// Removes all measurement results for a grid from their respective scopes. - /// - /// The for a grid to be removed. - private void RemoveGridFromScopes(MeasurementCache cache) - { - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - RemoveFromGroup(scopeName, result); - } - } - - /// - /// Handles removing the from a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to remove the from. - /// The to remove from a scope. - private void RemoveFromGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); - - group.Remove(result); - if (!group.Results.Any()) - _groups.Remove(scopeName); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs deleted file mode 100644 index 93163f4a92..0000000000 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class GridLayoutTests - { - private const double Inf = double.PositiveInfinity; - - [Theory] - [InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] - public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] - public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] - [InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] - [InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] - public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] - [InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] - [InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] - public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - - /// - /// This is needed because Mono somehow converts double array to object array in attribute metadata - /// - static void AssertEqual(IList expected, IReadOnlyList actual) - { - var conv = expected.Cast().ToArray(); - Assert.Equal(conv, actual); - } - - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] - private static void TestRowDefinitionsOnly(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] - [InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] - [InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] - public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, - IList expectedMeasureList, IList expectedArrangeList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(Inf); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(measure.DesiredLength, measure); - AssertEqual(expectedArrangeList, arrange.LengthList); - } - - [Theory] - [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] - public void MeasureArrange_ChildHasSize_Correct(string length, - IList childLengthList, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), - x => (double)childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] - [InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] - [InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] - [InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] - [InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] - public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( - double containerLength, double expectedDesiredLength, - IList expectedMeasureLengthList, IList expectedArrangeLengthList) - { - var length = "100,*,2*"; - var childLengthList = new[] { 150d, 150d, 150d }; - var spans = new[] { 1, 2, 1 }; - - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), - x => childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange( - double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, - measure); - AssertEqual(expectedArrangeLengthList, arrange.LengthList); - } - } -}