diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs new file mode 100644 index 0000000000..3a4b41314f --- /dev/null +++ b/src/Avalonia.Controls/GridWPF.cs @@ -0,0 +1,4420 @@ +// 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 JetBrains.Annotations; + + +using MS.Internal; +using MS.Internal.Controls; +using MS.Internal.PresentationFramework; +using MS.Internal.Telemetry.PresentationFramework; +using MS.Utility; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows.Threading; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Markup; + +#pragma warning disable 1634, 1691 // suppressing PreSharp warnings + +namespace System.Windows.Controls +{ + /// + /// Grid + /// + public class Grid : Panel, IAddChild + { + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + static Grid() + { + ControlsTraceLogger.AddControl(TelemetryControls.Grid); + } + + /// + /// Default constructor. + /// + public Grid() + { + SetFlags((bool) ShowGridLinesProperty.GetDefaultValue(DependencyObjectType), Flags.ShowGridLinesPropertyValue); + } + + #endregion Constructors + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + /// + /// + /// + void IAddChild.AddChild(object value) + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + UIElement cell = value as UIElement; + if (cell != null) + { + Children.Add(cell); + return; + } + + throw (new ArgumentException(SR.Get(SRID.Grid_UnexpectedParameterType, value.GetType(), typeof(UIElement)), "value")); + } + + /// + /// + /// + void IAddChild.AddText(string text) + { + XamlSerializerUtil.ThrowIfNonWhiteSpaceInAddText(text, this); + } + + /// + /// + /// + protected internal override IEnumerator LogicalChildren + { + get + { + // empty panel or a panel being used as the items + // host has *no* logical children; give empty enumerator + bool noChildren = (base.VisualChildrenCount == 0) || IsItemsHost; + + if (noChildren) + { + ExtendedData extData = ExtData; + + if ( extData == null + || ( (extData.ColumnDefinitions == null || extData.ColumnDefinitions.Count == 0) + && (extData.RowDefinitions == null || extData.RowDefinitions.Count == 0) ) + ) + { + // grid is empty + return EmptyEnumerator.Instance; + } + } + + return (new GridChildrenCollectionEnumeratorSimple(this, !noChildren)); + } + } + + /// + /// Helper for setting Column property on a UIElement. + /// + /// UIElement to set Column property on. + /// Column property value. + public static void SetColumn(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(ColumnProperty, value); + } + + /// + /// Helper for reading Column property from a UIElement. + /// + /// UIElement to read Column property from. + /// Column property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetColumn(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(ColumnProperty)); + } + + /// + /// Helper for setting Row property on a UIElement. + /// + /// UIElement to set Row property on. + /// Row property value. + public static void SetRow(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(RowProperty, value); + } + + /// + /// Helper for reading Row property from a UIElement. + /// + /// UIElement to read Row property from. + /// Row property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetRow(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(RowProperty)); + } + + /// + /// Helper for setting ColumnSpan property on a UIElement. + /// + /// UIElement to set ColumnSpan property on. + /// ColumnSpan property value. + public static void SetColumnSpan(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(ColumnSpanProperty, value); + } + + /// + /// Helper for reading ColumnSpan property from a UIElement. + /// + /// UIElement to read ColumnSpan property from. + /// ColumnSpan property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetColumnSpan(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(ColumnSpanProperty)); + } + + /// + /// Helper for setting RowSpan property on a UIElement. + /// + /// UIElement to set RowSpan property on. + /// RowSpan property value. + public static void SetRowSpan(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(RowSpanProperty, value); + } + + /// + /// Helper for reading RowSpan property from a UIElement. + /// + /// UIElement to read RowSpan property from. + /// RowSpan property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetRowSpan(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(RowSpanProperty)); + } + + /// + /// Helper for setting IsSharedSizeScope property on a UIElement. + /// + /// UIElement to set IsSharedSizeScope property on. + /// IsSharedSizeScope property value. + public static void SetIsSharedSizeScope(UIElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(IsSharedSizeScopeProperty, value); + } + + /// + /// Helper for reading IsSharedSizeScope property from a UIElement. + /// + /// UIElement to read IsSharedSizeScope property from. + /// IsSharedSizeScope property value. + public static bool GetIsSharedSizeScope(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((bool)element.GetValue(IsSharedSizeScopeProperty)); + } + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + #region Public Properties + + /// + /// ShowGridLines property. + /// + public bool ShowGridLines + { + get { return (CheckFlagsAnd(Flags.ShowGridLinesPropertyValue)); } + set { SetValue(ShowGridLinesProperty, value); } + } + + /// + /// Returns a ColumnDefinitionCollection of column definitions. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public ColumnDefinitionCollection ColumnDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitionCollection(this); } + + return (_data.ColumnDefinitions); + } + } + + /// + /// Returns a RowDefinitionCollection of row definitions. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public RowDefinitionCollection RowDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitionCollection(this); } + + return (_data.RowDefinitions); + } + } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + /// + /// Derived class must implement to support Visual children. The method must return + /// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1. + /// + /// By default a Visual does not have any children. + /// + /// Remark: + /// During this virtual call it is not valid to modify the Visual tree. + /// + protected override Visual GetVisualChild(int index) + { + // because "base.Count + 1" for GridLinesRenderer + // argument checking done at the base class + if(index == base.VisualChildrenCount) + { + if (_gridLinesRenderer == null) + { + throw new ArgumentOutOfRangeException("index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)); + } + return _gridLinesRenderer; + } + else return base.GetVisualChild(index); + } + + /// + /// Derived classes override this property to enable the Visual code to enumerate + /// the Visual children. Derived classes need to return the number of children + /// from this method. + /// + /// By default a Visual does not have any children. + /// + /// Remark: During this virtual method the Visual tree must not be modified. + /// + protected override int VisualChildrenCount + { + //since GridLinesRenderer has not been added as a child, so we do not subtract + get { return base.VisualChildrenCount + (_gridLinesRenderer != null ? 1 : 0); } + } + + + /// + /// Content measurement. + /// + /// Constraint + /// Desired size + protected override Size MeasureOverride(Size constraint) + { + Size gridDesiredSize; + ExtendedData extData = ExtData; + + try + { + EnterCounterScope(Counters.MeasureOverride); + + ListenToNotifications = true; + MeasureOverrideInProgress = true; + + if (extData == null) + { + gridDesiredSize = new Size(); + UIElementCollection children = InternalChildren; + + for (int i = 0, count = children.Count; i < count; ++i) + { + UIElement child = children[i]; + if (child != null) + { + child.Measure(constraint); + gridDesiredSize.Width = Math.Max(gridDesiredSize.Width, child.DesiredSize.Width); + gridDesiredSize.Height = 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 (RowDefinitionCollectionDirty || ColumnDefinitionCollectionDirty) + { + if (_definitionIndices != null) + { + Array.Clear(_definitionIndices, 0, _definitionIndices.Length); + _definitionIndices = null; + } + + if (UseLayoutRounding) + { + if (_roundingErrors != null) + { + Array.Clear(_roundingErrors, 0, _roundingErrors.Length); + _roundingErrors = null; + } + } + } + + ValidateDefinitionsUStructure(); + ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); + + ValidateDefinitionsVStructure(); + ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); + + CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); + + SizeToContentU = sizeToContentU; + SizeToContentV = sizeToContentV; + } + + ValidateCells(); + + Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); + + // Grid classifies cells into four groups depending on + // the column / row type a cell belongs to (number corresponds to + // group number): + // + // Px Auto Star + // +--------+--------+--------+ + // | | | | + // Px | 1 | 1 | 3 | + // | | | | + // +--------+--------+--------+ + // | | | | + // Auto | 1 | 1 | 3 | + // | | | | + // +--------+--------+--------+ + // | | | | + // Star | 4 | 2 | 4 | + // | | | | + // +--------+--------+--------+ + // + // The group number indicates the order in which cells are measured. + // Certain order is necessary to be able to dynamically resolve star + // columns / rows sizes which are used as input for measuring of + // the cells belonging to them. + // + // However, there are cases when topology of a grid causes cyclical + // size dependences. For example: + // + // + // column width="Auto" column width="*" + // +----------------------+----------------------+ + // | | | + // | | | + // | | | + // | | | + // row height="Auto" | | cell 1 2 | + // | | | + // | | | + // | | | + // | | | + // +----------------------+----------------------+ + // | | | + // | | | + // | | | + // | | | + // row height="*" | cell 2 1 | | + // | | | + // | | | + // | | | + // | | | + // +----------------------+----------------------+ + // + // In order to accurately calculate constraint width for "cell 1 2" + // (which is the remaining of grid's available width and calculated + // value of Auto column), "cell 2 1" needs to be calculated first, + // as it contributes to the Auto column's calculated value. + // At the same time in order to accurately calculate constraint + // height for "cell 2 1", "cell 1 2" needs to be calcualted first, + // as it contributes to Auto row height, which is used in the + // computation of Star row resolved height. + // + // to "break" this cyclical dependency we are making (arbitrary) + // decision to treat cells like "cell 2 1" as if they appear in Auto + // rows. And then recalculate them one more time when star row + // heights are resolved. + // + // (Or more strictly) the code below implement the following logic: + // + // +---------+ + // | enter | + // +---------+ + // | + // V + // +----------------+ + // | Measure Group1 | + // +----------------+ + // | + // V + // / - \ + // / \ + // Y / Can \ N + // +--------| Resolve |-----------+ + // | \ StarsV? / | + // | \ / | + // | \ - / | + // V V + // +----------------+ / - \ + // | Resolve StarsV | / \ + // +----------------+ Y / Can \ N + // | +----| Resolve |------+ + // V | \ StarsU? / | + // +----------------+ | \ / | + // | Measure Group2 | | \ - / | + // +----------------+ | V + // | | +-----------------+ + // V | | Measure Group2' | + // +----------------+ | +-----------------+ + // | Resolve StarsU | | | + // +----------------+ V V + // | +----------------+ +----------------+ + // V | Resolve StarsU | | Resolve StarsU | + // +----------------+ +----------------+ +----------------+ + // | Measure Group3 | | | + // +----------------+ V V + // | +----------------+ +----------------+ + // | | Measure Group3 | | Measure Group3 | + // | +----------------+ +----------------+ + // | | | + // | V V + // | +----------------+ +----------------+ + // | | Resolve StarsV | | Resolve StarsV | + // | +----------------+ +----------------+ + // | | | + // | | V + // | | +------------------+ + // | | | Measure Group2'' | + // | | +------------------+ + // | | | + // +----------------------+-------------------------+ + // | + // V + // +----------------+ + // | Measure Group4 | + // +----------------+ + // | + // V + // +--------+ + // | exit | + // +--------+ + // + // where: + // * all [Measure GroupN] - regular children measure process - + // each cell is measured given contraint size as an input + // and each cell's desired size is accumulated on the + // corresponding column / row; + // * [Measure Group2'] - is when each cell is measured with + // infinit height as a constraint and a cell's desired + // height is ignored; + // * [Measure Groups''] - is when each cell is measured (second + // time during single Grid.MeasureOverride) regularly but its + // returned width is ignored; + // + // This algorithm is believed to be as close to ideal as possible. + // It has the following drawbacks: + // * cells belonging to Group2 can be called to measure twice; + // * iff during second measure a cell belonging to Group2 returns + // desired width greater than desired width returned the first + // time, such a cell is going to be clipped, even though it + // appears in Auto column. + // + + MeasureCellsGroup(extData.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(extData.CellGroup2, constraint, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + } + else + { + // if at least one cell exists in Group2, it must be measured before + // StarsU can be resolved. + bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length; + if (canResolveStarsU) + { + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.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(extData.CellGroup2, false); + double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true); + + MeasureCellsGroup(extData.CellGroup2, constraint, false, true); + + do + { + if (hasDesiredSizeUChanged) + { + // Reset cached Group3Heights + ApplyCachedMinSizes(group3MinSizes, true); + } + + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + + // Reset cached Group2Widths + ApplyCachedMinSizes(group2MinSizes, false); + + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + } + while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); + } + } + } + + MeasureCellsGroup(extData.CellGroup4, constraint, false, false); + + EnterCounter(Counters._CalculateDesiredSize); + gridDesiredSize = new Size( + CalculateDesiredSize(DefinitionsU), + CalculateDesiredSize(DefinitionsV)); + ExitCounter(Counters._CalculateDesiredSize); + } + } + finally + { + MeasureOverrideInProgress = false; + ExitCounterScope(Counters.MeasureOverride); + } + + return (gridDesiredSize); + } + + /// + /// Content arrangement. + /// + /// Arrange size + protected override Size ArrangeOverride(Size arrangeSize) + { + try + { + EnterCounterScope(Counters.ArrangeOverride); + + ArrangeOverrideInProgress = true; + + if (_data == null) + { + UIElementCollection children = InternalChildren; + + for (int i = 0, count = children.Count; i < count; ++i) + { + UIElement child = children[i]; + if (child != null) + { + child.Arrange(new Rect(arrangeSize)); + } + } + } + else + { + Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); + + EnterCounter(Counters._SetFinalSize); + + SetFinalSize(DefinitionsU, arrangeSize.Width, true); + SetFinalSize(DefinitionsV, arrangeSize.Height, false); + + ExitCounter(Counters._SetFinalSize); + + UIElementCollection children = InternalChildren; + + for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) + { + UIElement cell = children[currentCell]; + if (cell == null) + { + continue; + } + + int columnIndex = PrivateCells[currentCell].ColumnIndex; + int rowIndex = PrivateCells[currentCell].RowIndex; + int columnSpan = PrivateCells[currentCell].ColumnSpan; + int rowSpan = PrivateCells[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) ); + + EnterCounter(Counters._ArrangeChildHelper2); + cell.Arrange(cellRect); + ExitCounter(Counters._ArrangeChildHelper2); + } + + // update render bound on grid lines renderer visual + GridLinesRenderer gridLinesRenderer = EnsureGridLinesRenderer(); + if (gridLinesRenderer != null) + { + gridLinesRenderer.UpdateRenderBounds(arrangeSize); + } + } + } + finally + { + SetValid(); + ArrangeOverrideInProgress = false; + ExitCounterScope(Counters.ArrangeOverride); + } + return (arrangeSize); + } + + /// + /// + /// + protected internal override void OnVisualChildrenChanged( + DependencyObject visualAdded, + DependencyObject visualRemoved) + { + CellsStructureDirty = true; + + base.OnVisualChildrenChanged(visualAdded, visualRemoved); + } + + #endregion Protected Methods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + /// + /// Invalidates grid caches and makes the grid dirty for measure. + /// + internal void Invalidate() + { + CellsStructureDirty = true; + InvalidateMeasure(); + } + + /// + /// 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; + + Invariant.Assert(_data != null); + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionCollectionDirty) + { + DefinitionBase[] definitions = DefinitionsU; + value = definitions[(columnIndex + 1) % definitions.Length].FinalOffset; + if (columnIndex != 0) { value -= definitions[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; + + Invariant.Assert(_data != null); + + // actual value calculations require structure to be up-to-date + if (!RowDefinitionCollectionDirty) + { + DefinitionBase[] definitions = DefinitionsV; + value = definitions[(rowIndex + 1) % definitions.Length].FinalOffset; + if (rowIndex != 0) { value -= definitions[rowIndex].FinalOffset; } + } + return (value); + } + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + /// + /// Convenience accessor to MeasureOverrideInProgress bit flag. + /// + internal bool MeasureOverrideInProgress + { + get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); } + set { SetFlags(value, Flags.MeasureOverrideInProgress); } + } + + /// + /// Convenience accessor to ArrangeOverrideInProgress bit flag. + /// + internal bool ArrangeOverrideInProgress + { + get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); } + set { SetFlags(value, Flags.ArrangeOverrideInProgress); } + } + + /// + /// Convenience accessor to ValidDefinitionsUStructure bit flag. + /// + internal bool ColumnDefinitionCollectionDirty + { + get { return (!CheckFlagsAnd(Flags.ValidDefinitionsUStructure)); } + set { SetFlags(!value, Flags.ValidDefinitionsUStructure); } + } + + /// + /// Convenience accessor to ValidDefinitionsVStructure bit flag. + /// + internal bool RowDefinitionCollectionDirty + { + get { return (!CheckFlagsAnd(Flags.ValidDefinitionsVStructure)); } + set { SetFlags(!value, Flags.ValidDefinitionsVStructure); } + } + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + #region Private Methods + + /// + /// Lays out cells according to rows and columns, and creates lookup grids. + /// + private void ValidateCells() + { + EnterCounter(Counters._ValidateCells); + + if (CellsStructureDirty) + { + ValidateCellsCore(); + CellsStructureDirty = false; + } + + ExitCounter(Counters._ValidateCells); + } + + /// + /// ValidateCellsCore + /// + private void ValidateCellsCore() + { + UIElementCollection children = InternalChildren; + ExtendedData extData = ExtData; + + extData.CellCachesCollection = new CellCache[children.Count]; + extData.CellGroup1 = int.MaxValue; + extData.CellGroup2 = int.MaxValue; + extData.CellGroup3 = int.MaxValue; + extData.CellGroup4 = int.MaxValue; + + bool hasStarCellsU = false; + bool hasStarCellsV = false; + bool hasGroup3CellsInAutoRows = false; + + for (int i = PrivateCells.Length - 1; i >= 0; --i) + { + UIElement child = children[i]; + if (child == null) + { + continue; + } + + CellCache cell = new CellCache(); + + // + // read and cache child positioning properties + // + + // 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 = extData.CellGroup1; + extData.CellGroup1 = i; + } + else + { + cell.Next = extData.CellGroup3; + extData.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 = extData.CellGroup2; + extData.CellGroup2 = i; + } + else + { + cell.Next = extData.CellGroup4; + extData.CellGroup4 = i; + } + } + + PrivateCells[i] = cell; + } + + HasStarCellsU = hasStarCellsU; + HasStarCellsV = hasStarCellsV; + HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; + } + + /// + /// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection + /// or to a default single element collection. DefinitionsU gets trimmed to size. + /// + /// + /// This is one of two methods, where ColumnDefinitions and DefinitionsU are directly accessed. + /// All the rest measure / arrange / render code must use DefinitionsU. + /// + private void ValidateDefinitionsUStructure() + { + EnterCounter(Counters._ValidateColsStructure); + + if (ColumnDefinitionCollectionDirty) + { + ExtendedData extData = ExtData; + + if (extData.ColumnDefinitions == null) + { + if (extData.DefinitionsU == null) + { + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() }; + } + } + else + { + extData.ColumnDefinitions.InternalTrimToSize(); + + if (extData.ColumnDefinitions.InternalCount == 0) + { + // if column definitions collection is empty + // mockup array with one column + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() }; + } + else + { + extData.DefinitionsU = extData.ColumnDefinitions.InternalItems; + } + } + + ColumnDefinitionCollectionDirty = false; + } + + Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Length > 0); + + ExitCounter(Counters._ValidateColsStructure); + } + + /// + /// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection + /// or to a default single element collection. DefinitionsV gets trimmed to size. + /// + /// + /// This is one of two methods, where RowDefinitions and DefinitionsV are directly accessed. + /// All the rest measure / arrange / render code must use DefinitionsV. + /// + private void ValidateDefinitionsVStructure() + { + EnterCounter(Counters._ValidateRowsStructure); + + if (RowDefinitionCollectionDirty) + { + ExtendedData extData = ExtData; + + if (extData.RowDefinitions == null) + { + if (extData.DefinitionsV == null) + { + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() }; + } + } + else + { + extData.RowDefinitions.InternalTrimToSize(); + + if (extData.RowDefinitions.InternalCount == 0) + { + // if row definitions collection is empty + // mockup array with one row + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() }; + } + else + { + extData.DefinitionsV = extData.RowDefinitions.InternalItems; + } + } + + RowDefinitionCollectionDirty = false; + } + + Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Length > 0); + + ExitCounter(Counters._ValidateRowsStructure); + } + + /// + /// 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) + { + definitions[i].OnBeforeLayout(this); + + 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 + /// 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 >= PrivateCells.Length) + { + return; + } + + UIElementCollection children = InternalChildren; + Hashtable spanStore = null; + bool ignoreDesiredSizeV = forceInfinityV; + + int i = cellsHead; + do + { + double oldWidth = children[i].DesiredSize.Width; + + MeasureCell(i, forceInfinityV); + + hasDesiredSizeUChanged |= !DoubleUtil.AreClose(oldWidth, children[i].DesiredSize.Width); + + if (!ignoreDesiredSizeU) + { + if (PrivateCells[i].ColumnSpan == 1) + { + DefinitionsU[PrivateCells[i].ColumnIndex].UpdateMinSize(Math.Min(children[i].DesiredSize.Width, DefinitionsU[PrivateCells[i].ColumnIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + PrivateCells[i].ColumnIndex, + PrivateCells[i].ColumnSpan, + true, + children[i].DesiredSize.Width); + } + } + + if (!ignoreDesiredSizeV) + { + if (PrivateCells[i].RowSpan == 1) + { + DefinitionsV[PrivateCells[i].RowIndex].UpdateMinSize(Math.Min(children[i].DesiredSize.Height, DefinitionsV[PrivateCells[i].RowIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + PrivateCells[i].RowIndex, + PrivateCells[i].RowSpan, + false, + children[i].DesiredSize.Height); + } + } + + i = PrivateCells[i].Next; + } while (i < PrivateCells.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) + { + EnterCounter(Counters._MeasureCell); + + double cellMeasureWidth; + double cellMeasureHeight; + + if ( PrivateCells[cell].IsAutoU + && !PrivateCells[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, + PrivateCells[cell].ColumnIndex, + PrivateCells[cell].ColumnSpan); + } + + if (forceInfinityV) + { + cellMeasureHeight = double.PositiveInfinity; + } + else if ( PrivateCells[cell].IsAutoV + && !PrivateCells[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, + PrivateCells[cell].RowIndex, + PrivateCells[cell].RowSpan); + } + + EnterCounter(Counters.__MeasureChild); + UIElement child = InternalChildren[cell]; + if (child != null) + { + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); + } + ExitCounter(Counters.__MeasureChild); + + ExitCounter(Counters._MeasureCell); + } + + + /// + /// 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 (!_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, s_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(_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, s_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(_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 + && !_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) + && !DoubleUtil.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !DoubleUtil.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); + } + } + } + } + } + } + + /// + /// 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) + { + if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) + { + ResolveStarLegacy(definitions, availableSize); + } + else + { + ResolveStarMaxDiscrepancy(definitions, availableSize); + } + } + + // original implementation, used from 3.0 through 4.6.2 + private void ResolveStarLegacy( + DefinitionBase[] definitions, + double availableSize) + { + DefinitionBase[] tempDefinitions = TempDefinitions; + int starDefinitionsCount = 0; + double takenSize = 0; + + for (int i = 0; i < definitions.Length; ++i) + { + switch (definitions[i].SizeType) + { + case (LayoutTimeSizeType.Auto): + takenSize += definitions[i].MinSize; + break; + case (LayoutTimeSizeType.Pixel): + takenSize += definitions[i].MeasureSize; + break; + case (LayoutTimeSizeType.Star): + { + tempDefinitions[starDefinitionsCount++] = definitions[i]; + + double starValue = definitions[i].UserSize.Value; + + if (_IsZero(starValue)) + { + definitions[i].MeasureSize = 0; + definitions[i].SizeCache = 0; + } + else + { + // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values + // can be summed up without overflow + starValue = Math.Min(starValue, c_starClip); + + // Note: normalized star value is temporary cached into MeasureSize + definitions[i].MeasureSize = starValue; + double maxSize = Math.Max(definitions[i].MinSize, definitions[i].UserMaxSize); + maxSize = Math.Min(maxSize, c_starClip); + definitions[i].SizeCache = maxSize / starValue; + } + } + break; + } + } + + if (starDefinitionsCount > 0) + { + Array.Sort(tempDefinitions, 0, starDefinitionsCount, s_starDistributionOrderComparer); + + // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... + // partial sum value is stored in each definition's SizeCache member. + // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus + // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. + // this is an important change from previous implementation where the following was possible: + // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... + double allStarWeights = 0; + int i = starDefinitionsCount - 1; + do + { + allStarWeights += tempDefinitions[i].MeasureSize; + tempDefinitions[i].SizeCache = allStarWeights; + } while (--i >= 0); + + i = 0; + do + { + double resolvedSize; + double starValue = tempDefinitions[i].MeasureSize; + + if (_IsZero(starValue)) + { + resolvedSize = tempDefinitions[i].MinSize; + } + else + { + double userSize = Math.Max(availableSize - takenSize, 0.0) * (starValue / tempDefinitions[i].SizeCache); + resolvedSize = Math.Min(userSize, tempDefinitions[i].UserMaxSize); + resolvedSize = Math.Max(tempDefinitions[i].MinSize, resolvedSize); + } + + tempDefinitions[i].MeasureSize = resolvedSize; + takenSize += resolvedSize; + } while (++i < starDefinitionsCount); + } + } + + // 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. + private void ResolveStarMaxDiscrepancy( + 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 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 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, s_minRatioComparer); + Array.Sort(tempDefinitions, defCount, maxCount, s_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 0) + { + Array.Sort(tempDefinitions, 0, starCount, s_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) + { + if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) + { + SetFinalSizeLegacy(definitions, finalSize, columns); + } + else + { + SetFinalSizeMaxDiscrepancy(definitions, finalSize, columns); + } + } + + // original implementation, used from 3.0 through 4.6.2 + private void SetFinalSizeLegacy( + DefinitionBase[] definitions, + double finalSize, + bool columns) + { + int starDefinitionsCount = 0; // traverses form the first entry up + int nonStarIndex = definitions.Length; // traverses from the last entry down + double allPreferredArrangeSize = 0; + bool useLayoutRounding = this.UseLayoutRounding; + int[] definitionIndices = DefinitionIndices; + double[] roundingErrors = null; + + // If using layout rounding, check whether rounding needs to compensate for high DPI + double dpi = 1.0; + + if (useLayoutRounding) + { + DpiScale dpiScale = GetDpi(); + dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; + roundingErrors = RoundingErrors; + } + + for (int i = 0; i < definitions.Length; ++i) + { + // if definition is shared then is cannot be star + Debug.Assert(!definitions[i].IsShared || !definitions[i].UserSize.IsStar); + + if (definitions[i].UserSize.IsStar) + { + double starValue = definitions[i].UserSize.Value; + + if (_IsZero(starValue)) + { + // cach normilized star value temporary into MeasureSize + definitions[i].MeasureSize = 0; + definitions[i].SizeCache = 0; + } + else + { + // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values + // can be summed up without overflow + starValue = Math.Min(starValue, c_starClip); + + // Note: normalized star value is temporary cached into MeasureSize + definitions[i].MeasureSize = starValue; + double maxSize = Math.Max(definitions[i].MinSizeForArrange, definitions[i].UserMaxSize); + maxSize = Math.Min(maxSize, c_starClip); + definitions[i].SizeCache = maxSize / starValue; + if (useLayoutRounding) + { + roundingErrors[i] = definitions[i].SizeCache; + definitions[i].SizeCache = UIElement.RoundLayoutValue(definitions[i].SizeCache, dpi); + } + } + definitionIndices[starDefinitionsCount++] = i; + } + else + { + double userSize = 0; + + switch (definitions[i].UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + userSize = definitions[i].UserSize.Value; + break; + + case (GridUnitType.Auto): + userSize = definitions[i].MinSizeForArrange; + break; + } + + double userMaxSize; + + if (definitions[i].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 = definitions[i].UserMaxSize; + } + + definitions[i].SizeCache = Math.Max(definitions[i].MinSizeForArrange, Math.Min(userSize, userMaxSize)); + if (useLayoutRounding) + { + roundingErrors[i] = definitions[i].SizeCache; + definitions[i].SizeCache = UIElement.RoundLayoutValue(definitions[i].SizeCache, dpi); + } + + allPreferredArrangeSize += definitions[i].SizeCache; + definitionIndices[--nonStarIndex] = i; + } + } + + // indices should meet + Debug.Assert(nonStarIndex == starDefinitionsCount); + + if (starDefinitionsCount > 0) + { + StarDistributionOrderIndexComparer starDistributionOrderIndexComparer = new StarDistributionOrderIndexComparer(definitions); + Array.Sort(definitionIndices, 0, starDefinitionsCount, starDistributionOrderIndexComparer); + + // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... + // partial sum value is stored in each definition's SizeCache member. + // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus + // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. + // this is an important change from previous implementation where the following was possible: + // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... + double allStarWeights = 0; + int i = starDefinitionsCount - 1; + do + { + allStarWeights += definitions[definitionIndices[i]].MeasureSize; + definitions[definitionIndices[i]].SizeCache = allStarWeights; + } while (--i >= 0); + + i = 0; + do + { + double resolvedSize; + double starValue = definitions[definitionIndices[i]].MeasureSize; + + if (_IsZero(starValue)) + { + resolvedSize = definitions[definitionIndices[i]].MinSizeForArrange; + } + else + { + double userSize = Math.Max(finalSize - allPreferredArrangeSize, 0.0) * (starValue / definitions[definitionIndices[i]].SizeCache); + resolvedSize = Math.Min(userSize, definitions[definitionIndices[i]].UserMaxSize); + resolvedSize = Math.Max(definitions[definitionIndices[i]].MinSizeForArrange, resolvedSize); + } + + definitions[definitionIndices[i]].SizeCache = resolvedSize; + if (useLayoutRounding) + { + roundingErrors[definitionIndices[i]] = definitions[definitionIndices[i]].SizeCache; + definitions[definitionIndices[i]].SizeCache = UIElement.RoundLayoutValue(definitions[definitionIndices[i]].SizeCache, dpi); + } + + allPreferredArrangeSize += definitions[definitionIndices[i]].SizeCache; + } while (++i < starDefinitionsCount); + } + + if ( allPreferredArrangeSize > finalSize + && !_AreClose(allPreferredArrangeSize, finalSize) ) + { + DistributionOrderIndexComparer distributionOrderIndexComparer = new DistributionOrderIndexComparer(definitions); + Array.Sort(definitionIndices, 0, definitions.Length, distributionOrderIndexComparer); + double sizeToDistribute = finalSize - allPreferredArrangeSize; + + for (int i = 0; i < definitions.Length; ++i) + { + int definitionIndex = definitionIndices[i]; + double final = definitions[definitionIndex].SizeCache + (sizeToDistribute / (definitions.Length - i)); + double finalOld = final; + final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); + final = Math.Min(final, definitions[definitionIndex].SizeCache); + + if (useLayoutRounding) + { + roundingErrors[definitionIndex] = final; + final = UIElement.RoundLayoutValue(finalOld, dpi); + final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); + final = Math.Min(final, definitions[definitionIndex].SizeCache); + } + + sizeToDistribute -= (final - definitions[definitionIndex].SizeCache); + definitions[definitionIndex].SizeCache = final; + } + + allPreferredArrangeSize = finalSize - sizeToDistribute; + } + + if (useLayoutRounding) + { + if (!_AreClose(allPreferredArrangeSize, finalSize)) + { + // Compute deltas + for (int i = 0; i < definitions.Length; ++i) + { + roundingErrors[i] = roundingErrors[i] - definitions[i].SizeCache; + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); + double adjustedSize = allPreferredArrangeSize; + double dpiIncrement = UIElement.RoundLayoutValue(1.0, dpi); + + if (allPreferredArrangeSize > finalSize) + { + int i = definitions.Length - 1; + while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (allPreferredArrangeSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Length) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final > definition.SizeCache) + { + adjustedSize += dpiIncrement; + } + definition.SizeCache = final; + i++; + } + } + } + } + + 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; + } + } + + // new implementation, as of 4.7. This incorporates the same algorithm + // as in ResolveStarMaxDiscrepancy. It differs in the same way that SetFinalSizeLegacy + // differs from ResolveStarLegacy, namely (a) leaves results in def.SizeCache + // instead of def.MeasureSize, (b) implements LayoutRounding if requested, + // (c) stores intermediate results differently. + // The LayoutRounding logic is improved: + // 1. Use pre-rounded values during proportional allocation. This avoids the + // same kind of problems arising from interaction with min/max that + // motivated the new algorithm in the first place. + // 2. Use correct "nudge" amount when distributing roundoff space. This + // comes into play at high DPI - greater than 134. + // 3. Applies rounding only to real pixel values (not to ratios) + private void SetFinalSizeMaxDiscrepancy( + 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 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 0.0) + { + // store ratio w/min in MeasureSize (for now) + definitionIndices[minCount++] = i; + def.MeasureSize = starWeight / def.MinSizeForArrange; + } + + double effectiveMaxSize = Math.Max(def.MinSizeForArrange, 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.MinSizeForArrange; + 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.MinSizeForArrange, 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.MinSizeForArrange; + --minCount; + } + else + { + resolvedIndex = definitionIndices[defCount + maxCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = Math.Max(resolvedDef.MinSizeForArrange, 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 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.MinSizeForArrange, 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) + { + DpiScale dpiScale = GetDpi(); + double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; + 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 = UIElement.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 (!_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 && !_AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (roundedTakenSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Length) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + 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() + { + ExtendedData extData = ExtData; + if (extData != null) + { +// for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); +// for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); + + if (extData.TempDefinitions != null) + { + // TempDefinitions has to be cleared to avoid "memory leaks" + Array.Clear(extData.TempDefinitions, 0, Math.Max(DefinitionsU.Length, DefinitionsV.Length)); + extData.TempDefinitions = null; + } + } + } + + /// + /// Returns true if ColumnDefinitions collection is not empty + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeColumnDefinitions() + { + ExtendedData extData = ExtData; + return ( extData != null + && extData.ColumnDefinitions != null + && extData.ColumnDefinitions.Count > 0 ); + } + + /// + /// Returns true if RowDefinitions collection is not empty + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeRowDefinitions() + { + ExtendedData extData = ExtData; + return ( extData != null + && extData.RowDefinitions != null + && extData.RowDefinitions.Count > 0 ); + } + + /// + /// 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.AddVisualChild(_gridLinesRenderer); + } + + if ((!ShowGridLines) && (_gridLinesRenderer != null)) + { + this.RemoveVisualChild(_gridLinesRenderer); + _gridLinesRenderer = null; + } + + return (_gridLinesRenderer); + } + + /// + /// SetFlags is used to set or unset one or multiple + /// flags on the object. + /// + private void SetFlags(bool value, Flags flags) + { + _flags = value ? (_flags | flags) : (_flags & (~flags)); + } + + /// + /// CheckFlagsAnd returns true if all the flags in the + /// given bitmask are set on the object. + /// + private bool CheckFlagsAnd(Flags flags) + { + return ((_flags & flags) == flags); + } + + /// + /// CheckFlagsOr returns true if at least one flag in the + /// given bitmask is set. + /// + /// + /// If no bits are set in the given bitmask, the method returns + /// true. + /// + private bool CheckFlagsOr(Flags flags) + { + return (flags == 0 || (_flags & flags) != 0); + } + + /// + /// + /// + private static void OnShowGridLinesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Grid grid = (Grid)d; + + if ( grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + && grid.ListenToNotifications) + { + grid.InvalidateVisual(); + } + + grid.SetFlags((bool) e.NewValue, Flags.ShowGridLinesPropertyValue); + } + + /// + /// + /// + private static void OnCellAttachedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Visual child = d as Visual; + + if (child != null) + { + Grid grid = VisualTreeHelper.GetParent(child) as Grid; + if ( grid != null + && grid.ExtData != null + && grid.ListenToNotifications ) + { + grid.CellsStructureDirty = true; + grid.InvalidateMeasure(); + } + } + } + + /// + /// + /// + private static bool IsIntValueNotNegative(object value) + { + return ((int)value >= 0); + } + + /// + /// + /// + private static bool IsIntValueGreaterThanZero(object value) + { + return ((int)value > 0); + } + + /// + /// Helper for Comparer methods. + /// + /// + /// true iff 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); + } + + #endregion Private Methods + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + #region Private Properties + + /// + /// Private version returning array of column definitions. + /// + private DefinitionBase[] DefinitionsU + { + get { return (ExtData.DefinitionsU); } + } + + /// + /// Private version returning array of row definitions. + /// + private DefinitionBase[] DefinitionsV + { + get { return (ExtData.DefinitionsV); } + } + + /// + /// Helper accessor to layout time array of definitions. + /// + private DefinitionBase[] TempDefinitions + { + get + { + ExtendedData extData = ExtData; + int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length) * 2; + + if ( extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength ) + { + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(s_tempDefinitionsDataSlot); + if (tempDefinitionsWeakRef == null) + { + extData.TempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(s_tempDefinitionsDataSlot, new WeakReference(extData.TempDefinitions)); + } + else + { + extData.TempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if ( extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength ) + { + extData.TempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = extData.TempDefinitions; + } + } + } + return (extData.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; + } + } + + /// + /// Private version returning array of cells. + /// + private CellCache[] PrivateCells + { + get { return (ExtData.CellCachesCollection); } + } + + /// + /// Convenience accessor to ValidCellsStructure bit flag. + /// + private bool CellsStructureDirty + { + get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } + set { SetFlags(!value, Flags.ValidCellsStructure); } + } + + /// + /// Convenience accessor to ListenToNotifications bit flag. + /// + private bool ListenToNotifications + { + get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } + set { SetFlags(value, Flags.ListenToNotifications); } + } + + /// + /// Convenience accessor to SizeToContentU bit flag. + /// + private bool SizeToContentU + { + get { return (CheckFlagsAnd(Flags.SizeToContentU)); } + set { SetFlags(value, Flags.SizeToContentU); } + } + + /// + /// Convenience accessor to SizeToContentV bit flag. + /// + private bool SizeToContentV + { + get { return (CheckFlagsAnd(Flags.SizeToContentV)); } + set { SetFlags(value, Flags.SizeToContentV); } + } + + /// + /// Convenience accessor to HasStarCellsU bit flag. + /// + private bool HasStarCellsU + { + get { return (CheckFlagsAnd(Flags.HasStarCellsU)); } + set { SetFlags(value, Flags.HasStarCellsU); } + } + + /// + /// Convenience accessor to HasStarCellsV bit flag. + /// + private bool HasStarCellsV + { + get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } + set { SetFlags(value, Flags.HasStarCellsV); } + } + + /// + /// Convenience accessor to HasGroup3CellsInAutoRows bit flag. + /// + private bool HasGroup3CellsInAutoRows + { + get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } + set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } + } + + /// + /// fp version of d == 0. + /// + /// Value to check. + /// true if d == 0. + private static bool _IsZero(double d) + { + return (Math.Abs(d) < c_epsilon); + } + + /// + /// fp version of d1 == d2 + /// + /// First value to compare + /// Second value to compare + /// true if d1 == d2 + private static bool _AreClose(double d1, double d2) + { + return (Math.Abs(d1 - d2) < c_epsilon); + } + + /// + /// Returns reference to extended data bag. + /// + private ExtendedData ExtData + { + get { return (_data); } + } + + /// + /// 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; + } + } + + #endregion Private Properties + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + private ExtendedData _data; // extended data instantiated on demand, for non-trivial case handling only + private Flags _flags; // grid validity / property caches dirtiness flags + private GridLinesRenderer _gridLinesRenderer; + + // Keeps track of definition indices. + int[] _definitionIndices; + + // Stores unrounded values and rounding errors during layout rounding. + double[] _roundingErrors; + + #endregion Private Fields + + //------------------------------------------------------ + // + // Static Fields + // + //------------------------------------------------------ + + #region Static Fields + private const double c_epsilon = 1e-5; // used in fp calculations + private const double c_starClip = 1e298; // used as maximum for clipping star values during normalization + private const int c_layoutLoopMaxCount = 5; // 5 is an arbitrary constant chosen to end the measure loop + private static readonly LocalDataStoreSlot s_tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + private static readonly IComparer s_spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + private static readonly IComparer s_spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + private static readonly IComparer s_starDistributionOrderComparer = new StarDistributionOrderComparer(); + private static readonly IComparer s_distributionOrderComparer = new DistributionOrderComparer(); + private static readonly IComparer s_minRatioComparer = new MinRatioComparer(); + private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); + private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); + + #endregion Static Fields + + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ + + #region Private Structures Classes + + /// + /// Extended data instantiated on demand, when grid handles non-trivial case. + /// + private class ExtendedData + { + internal ColumnDefinitionCollection ColumnDefinitions; // collection of column definitions (logical tree support) + internal RowDefinitionCollection RowDefinitions; // collection of row definitions (logical tree support) + internal DefinitionBase[] DefinitionsU; // collection of column definitions used during calc + internal DefinitionBase[] DefinitionsV; // collection of row definitions used during calc + internal CellCache[] CellCachesCollection; // backing store for logical children + internal int CellGroup1; // index of the first cell in first cell group + internal int CellGroup2; // index of the first cell in second cell group + internal int CellGroup3; // index of the first cell in third cell group + internal int CellGroup4; // index of the first cell in forth cell group + internal DefinitionBase[] TempDefinitions; // temporary array used during layout for various purposes + // TempDefinitions.Length == Max(definitionsU.Length, definitionsV.Length) + } + + /// + /// Grid validity / property caches dirtiness flags + /// + [System.Flags] + private enum Flags + { + // + // the foolowing flags let grid tracking dirtiness in more granular manner: + // * Valid???Structure flags indicate that elements were added or removed. + // * Valid???Layout flags indicate that layout time portion of the information + // stored on the objects should be updated. + // + ValidDefinitionsUStructure = 0x00000001, + ValidDefinitionsVStructure = 0x00000002, + ValidCellsStructure = 0x00000004, + + // + // boolean properties state + // + ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? + + // + // boolean flags + // + ListenToNotifications = 0x00001000, // "0" when all notifications are ignored + SizeToContentU = 0x00002000, // "1" if calculating to content in U direction + SizeToContentV = 0x00004000, // "1" if calculating to content in V direction + HasStarCellsU = 0x00008000, // "1" if at least one cell belongs to a Star column + HasStarCellsV = 0x00010000, // "1" if at least one cell belongs to a Star row + HasGroup3CellsInAutoRows = 0x00020000, // "1" if at least one cell of group 3 belongs to an Auto row + MeasureOverrideInProgress = 0x00040000, // "1" while in the context of Grid.MeasureOverride + ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride + } + + #endregion Private Structures Classes + + //------------------------------------------------------ + // + // Properties + // + //------------------------------------------------------ + + #region Properties + + /// + /// ShowGridLines property. This property is used mostly + /// for simplification of visual debuggig. When it is set + /// to true grid lines are drawn to visualize location + /// of grid lines. + /// + public static readonly DependencyProperty ShowGridLinesProperty = + DependencyProperty.Register( + "ShowGridLines", + typeof(bool), + typeof(Grid), + new FrameworkPropertyMetadata( + false, + new PropertyChangedCallback(OnShowGridLinesPropertyChanged))); + + /// + /// Column property. This is an attached property. + /// Grid defines Column property, so that it can be set + /// on any element treated as a cell. Column property + /// specifies child's position with respect to columns. + /// + /// + /// Columns are 0 - based. In order to appear in first column, element + /// should have Column property set to 0. + /// Default value for the property is 0. + /// + [CommonDependencyProperty] + public static readonly DependencyProperty ColumnProperty = + DependencyProperty.RegisterAttached( + "Column", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 0, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueNotNegative)); + + /// + /// Row property. This is an attached property. + /// Grid defines Row, so that it can be set + /// on any element treated as a cell. Row property + /// specifies child's position with respect to rows. + /// + /// Rows are 0 - based. In order to appear in first row, element + /// should have Row property set to 0. + /// Default value for the property is 0. + /// + /// + [CommonDependencyProperty] + public static readonly DependencyProperty RowProperty = + DependencyProperty.RegisterAttached( + "Row", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 0, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueNotNegative)); + + /// + /// ColumnSpan property. This is an attached property. + /// Grid defines ColumnSpan, so that it can be set + /// on any element treated as a cell. ColumnSpan property + /// specifies child's width with respect to columns. + /// Example, ColumnSpan == 2 means that child will span across two columns. + /// + /// + /// Default value for the property is 1. + /// + [CommonDependencyProperty] + public static readonly DependencyProperty ColumnSpanProperty = + DependencyProperty.RegisterAttached( + "ColumnSpan", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 1, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueGreaterThanZero)); + + /// + /// RowSpan property. This is an attached property. + /// Grid defines RowSpan, so that it can be set + /// on any element treated as a cell. RowSpan property + /// specifies child's height with respect to row grid lines. + /// Example, RowSpan == 3 means that child will span across three rows. + /// + /// + /// Default value for the property is 1. + /// + [CommonDependencyProperty] + public static readonly DependencyProperty RowSpanProperty = + DependencyProperty.RegisterAttached( + "RowSpan", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 1, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueGreaterThanZero)); + + + /// + /// IsSharedSizeScope property marks scoping element for shared size. + /// + public static readonly DependencyProperty IsSharedSizeScopeProperty = + DependencyProperty.RegisterAttached( + "IsSharedSizeScope", + typeof(bool), + typeof(Grid), + new FrameworkPropertyMetadata( + false, + new PropertyChangedCallback(DefinitionBase.OnIsSharedSizeScopePropertyChanged))); + + #endregion Properties + + //------------------------------------------------------ + // + // Internal Structures / Classes + // + //------------------------------------------------------ + + #region Internal Structures Classes + + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } + + #endregion Internal Structures Classes + + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ + + #region Private Structures Classes + + /// + /// 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; + } + } + + /// + /// StarDistributionOrderComparer. + /// + private class StarDistributionOrderComparer : 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; + } + } + + /// + /// DistributionOrderComparer. + /// + private class DistributionOrderComparer: 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)) + { + double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; + double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; + result = xprime.CompareTo(yprime); + } + + return result; + } + } + + + /// + /// StarDistributionOrderIndexComparer. + /// + private class StarDistributionOrderIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal StarDistributionOrderIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(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; + } + } + + /// + /// DistributionOrderComparer. + /// + private class DistributionOrderIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal DistributionOrderIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(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)) + { + double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; + double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; + result = xprime.CompareTo(yprime); + } + + return result; + } + } + + /// + /// RoundingErrorIndexComparer. + /// + private class RoundingErrorIndexComparer : IComparer + { + private readonly double[] errors; + + internal RoundingErrorIndexComparer(double[] errors) + { + Invariant.Assert(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) + { + Invariant.Assert(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) + { + Invariant.Assert(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) + { + Invariant.Assert(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; + } + } + + /// + /// Implementation of a simple enumerator of grid's logical children + /// + private class GridChildrenCollectionEnumeratorSimple : IEnumerator + { + internal GridChildrenCollectionEnumeratorSimple(Grid grid, bool includeChildren) + { + Debug.Assert(grid != null); + _currentEnumerator = -1; + _enumerator0 = new ColumnDefinitionCollection.Enumerator(grid.ExtData != null ? grid.ExtData.ColumnDefinitions : null); + _enumerator1 = new RowDefinitionCollection.Enumerator(grid.ExtData != null ? grid.ExtData.RowDefinitions : null); + // GridLineRenderer is NOT included into this enumerator. + _enumerator2Index = 0; + if (includeChildren) + { + _enumerator2Collection = grid.Children; + _enumerator2Count = _enumerator2Collection.Count; + } + else + { + _enumerator2Collection = null; + _enumerator2Count = 0; + } + } + + public bool MoveNext() + { + while (_currentEnumerator < 3) + { + if (_currentEnumerator >= 0) + { + switch (_currentEnumerator) + { + case (0): if (_enumerator0.MoveNext()) { _currentChild = _enumerator0.Current; return (true); } break; + case (1): if (_enumerator1.MoveNext()) { _currentChild = _enumerator1.Current; return (true); } break; + case (2): if (_enumerator2Index < _enumerator2Count) + { + _currentChild = _enumerator2Collection[_enumerator2Index]; + _enumerator2Index++; + return (true); + } + break; + } + } + _currentEnumerator++; + } + return (false); + } + + public Object Current + { + get + { + if (_currentEnumerator == -1) + { + #pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception + throw new InvalidOperationException(SR.Get(SRID.EnumeratorNotStarted)); + } + if (_currentEnumerator >= 3) + { + #pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception + throw new InvalidOperationException(SR.Get(SRID.EnumeratorReachedEnd)); + } + + // assert below is not true anymore since UIElementCollection allowes for null children + //Debug.Assert(_currentChild != null); + return (_currentChild); + } + } + + public void Reset() + { + _currentEnumerator = -1; + _currentChild = null; + _enumerator0.Reset(); + _enumerator1.Reset(); + _enumerator2Index = 0; + } + + private int _currentEnumerator; + private Object _currentChild; + private ColumnDefinitionCollection.Enumerator _enumerator0; + private RowDefinitionCollection.Enumerator _enumerator1; + private UIElementCollection _enumerator2Collection; + private int _enumerator2Index; + private int _enumerator2Count; + } + + /// + /// Helper to render grid lines. + /// + internal class GridLinesRenderer : DrawingVisual + { + /// + /// Static initialization + /// + static GridLinesRenderer() + { + s_oddDashPen = new Pen(Brushes.Blue, c_penWidth); + DoubleCollection oddDashArray = new DoubleCollection(); + oddDashArray.Add(c_dashLength); + oddDashArray.Add(c_dashLength); + s_oddDashPen.DashStyle = new DashStyle(oddDashArray, 0); + s_oddDashPen.DashCap = PenLineCap.Flat; + s_oddDashPen.Freeze(); + + s_evenDashPen = new Pen(Brushes.Yellow, c_penWidth); + DoubleCollection evenDashArray = new DoubleCollection(); + evenDashArray.Add(c_dashLength); + evenDashArray.Add(c_dashLength); + s_evenDashPen.DashStyle = new DashStyle(evenDashArray, c_dashLength); + s_evenDashPen.DashCap = PenLineCap.Flat; + s_evenDashPen.Freeze(); + } + + /// + /// UpdateRenderBounds. + /// + /// Size of render bounds + internal void UpdateRenderBounds(Size boundsSize) + { + using (DrawingContext drawingContext = RenderOpen()) + { + Grid grid = VisualTreeHelper.GetParent(this) as Grid; + if ( grid == null + || grid.ShowGridLines == false ) + { + return; + } + + for (int i = 1; i < grid.DefinitionsU.Length; ++i) + { + DrawGridLine( + drawingContext, + grid.DefinitionsU[i].FinalOffset, 0.0, + grid.DefinitionsU[i].FinalOffset, boundsSize.Height); + } + + for (int i = 1; i < grid.DefinitionsV.Length; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.DefinitionsV[i].FinalOffset, + boundsSize.Width, grid.DefinitionsV[i].FinalOffset); + } + } + } + + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) + { + Point start = new Point(startX, startY); + Point end = new Point(endX, endY); + drawingContext.DrawLine(s_oddDashPen, start, end); + drawingContext.DrawLine(s_evenDashPen, start, end); + } + + 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 + private static readonly Point c_zeroPoint = new Point(0, 0); + } + + #endregion Private Structures Classes + + //------------------------------------------------------ + // + // Extended debugging for grid + // + //------------------------------------------------------ + +#if GRIDPARANOIA + private static double _performanceFrequency; + private static readonly bool _performanceFrequencyInitialized = InitializePerformanceFrequency(); + + //CASRemoval:[System.Security.SuppressUnmanagedCodeSecurity, System.Runtime.InteropServices.DllImport("kernel32.dll")] + private static extern bool QueryPerformanceCounter(out long lpPerformanceCount); + + //CASRemoval:[System.Security.SuppressUnmanagedCodeSecurity, System.Runtime.InteropServices.DllImport("kernel32.dll")] + private static extern bool QueryPerformanceFrequency(out long lpFrequency); + + private static double CostInMilliseconds(long count) + { + return ((double)count / _performanceFrequency); + } + + private static long Cost(long startCount, long endCount) + { + long l = endCount - startCount; + if (l < 0) { l += long.MaxValue; } + return (l); + } + + private static bool InitializePerformanceFrequency() + { + long l; + QueryPerformanceFrequency(out l); + _performanceFrequency = (double)l * 0.001; + return (true); + } + + private struct Counter + { + internal long Start; + internal long Total; + internal int Calls; + } + + private Counter[] _counters; + private bool _hasNewCounterInfo; +#endif // GRIDPARANOIA + + // + // This property + // 1. Finds the correct initial size for the _effectiveValues store on the current DependencyObject + // 2. This is a performance optimization + // + internal override int EffectiveValuesInitialSize + { + get { return 9; } + } + + [Conditional("GRIDPARANOIA")] + internal void EnterCounterScope(Counters scopeCounter) + { + #if GRIDPARANOIA + if (ID == "CountThis") + { + if (_counters == null) + { + _counters = new Counter[(int)Counters.Count]; + } + ExitCounterScope(Counters.Default); + EnterCounter(scopeCounter); + } + else + { + _counters = null; + } + #endif // GRIDPARANOIA + } + + [Conditional("GRIDPARANOIA")] + internal void ExitCounterScope(Counters scopeCounter) + { + #if GRIDPARANOIA + if (_counters != null) + { + if (scopeCounter != Counters.Default) + { + ExitCounter(scopeCounter); + } + + if (_hasNewCounterInfo) + { + string NFormat = "F6"; + Console.WriteLine( + "\ncounter name | total t (ms) | # of calls | per call t (ms)" + + "\n----------------------+---------------+---------------+----------------------" ); + + for (int i = 0; i < _counters.Length; ++i) + { + if (_counters[i].Calls > 0) + { + Counters counter = (Counters)i; + double total = CostInMilliseconds(_counters[i].Total); + double single = total / _counters[i].Calls; + string counterName = counter.ToString(); + string separator; + + if (counterName.Length < 8) { separator = "\t\t\t"; } + else if (counterName.Length < 16) { separator = "\t\t"; } + else { separator = "\t"; } + + Console.WriteLine( + counter.ToString() + separator + + total.ToString(NFormat) + "\t" + + _counters[i].Calls + "\t\t" + + single.ToString(NFormat)); + + _counters[i] = new Counter(); + } + } + } + _hasNewCounterInfo = false; + } + #endif // GRIDPARANOIA + } + + [Conditional("GRIDPARANOIA")] + internal void EnterCounter(Counters counter) + { + #if GRIDPARANOIA + if (_counters != null) + { + Debug.Assert((int)counter < _counters.Length); + + int i = (int)counter; + QueryPerformanceCounter(out _counters[i].Start); + } + #endif // GRIDPARANOIA + } + + [Conditional("GRIDPARANOIA")] + internal void ExitCounter(Counters counter) + { + #if GRIDPARANOIA + if (_counters != null) + { + Debug.Assert((int)counter < _counters.Length); + + int i = (int)counter; + long l; + QueryPerformanceCounter(out l); + l = Cost(_counters[i].Start, l); + _counters[i].Total += l; + _counters[i].Calls++; + _hasNewCounterInfo = true; + } + #endif // GRIDPARANOIA + } + + internal enum Counters : int + { + Default = -1, + + MeasureOverride, + _ValidateColsStructure, + _ValidateRowsStructure, + _ValidateCells, + _MeasureCell, + __MeasureChild, + _CalculateDesiredSize, + + ArrangeOverride, + _SetFinalSize, + _ArrangeChildHelper2, + _PositionCell, + + Count, + } + } +} \ No newline at end of file