// This source file is adapted from the Windows Presentation Foundation project. // (https://github.com/dotnet/wpf/) // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Avalonia; using Avalonia.Collections; using Avalonia.Utilities; namespace Avalonia.Controls { /// /// DefinitionBase provides core functionality used internally by Grid /// and ColumnDefinitionCollection / RowDefinitionCollection /// public abstract class DefinitionBase : AvaloniaObject { /// /// SharedSizeGroup property. /// public string SharedSizeGroup { get { return (string)GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } /// /// Callback to notify about entering model tree. /// internal void OnEnterParentTree() { this.InheritanceParent = Parent; if (_sharedState == null) { // start with getting SharedSizeGroup value. // this property is NOT inhereted which should result in better overall perf. string sharedSizeGroupId = SharedSizeGroup; if (sharedSizeGroupId != null) { SharedSizeScope privateSharedSizeScope = PrivateSharedSizeScope; if (privateSharedSizeScope != null) { _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); _sharedState.AddMember(this); } } } Parent?.InvalidateMeasure(); } /// /// Callback to notify about exitting model tree. /// internal void OnExitParentTree() { _offset = 0; if (_sharedState != null) { _sharedState.RemoveMember(this); _sharedState = null; } Parent?.InvalidateMeasure(); } /// /// Performs action preparing definition to enter layout calculation mode. /// internal void OnBeforeLayout(Grid grid) { // reset layout state. _minSize = 0; LayoutWasUpdated = true; // defer verification for shared definitions if (_sharedState != null) { _sharedState.EnsureDeferredValidation(grid); } } /// /// Updates min size. /// /// New size. internal void UpdateMinSize(double minSize) { _minSize = Math.Max(_minSize, minSize); } /// /// Sets min size. /// /// New size. internal void SetMinSize(double minSize) { _minSize = minSize; } /// /// This method reflects Grid.SharedScopeProperty state by setting / clearing /// dynamic property PrivateSharedSizeScopeProperty. Value of PrivateSharedSizeScopeProperty /// is a collection of SharedSizeState objects for the scope. /// internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { if ((bool)e.NewValue) { SharedSizeScope sharedStatesCollection = new SharedSizeScope(); d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); } else { d.ClearValue(PrivateSharedSizeScopeProperty); } } /// /// Returns true if this definition is a part of shared group. /// internal bool IsShared { get { return (_sharedState != null); } } /// /// Internal accessor to user size field. /// internal GridLength UserSize { get { return (_sharedState != null ? _sharedState.UserSize : UserSizeValueCache); } } /// /// Internal accessor to user min size field. /// internal double UserMinSize { get { return (UserMinSizeValueCache); } } /// /// Internal accessor to user max size field. /// internal double UserMaxSize { get { return (UserMaxSizeValueCache); } } /// /// DefinitionBase's index in the parents collection. /// internal int Index { get { return (_parentIndex); } set { Debug.Assert(value >= -1); _parentIndex = value; } } /// /// Layout-time user size type. /// internal Grid.LayoutTimeSizeType SizeType { get { return (_sizeType); } set { _sizeType = value; } } /// /// Returns or sets measure size for the definition. /// internal double MeasureSize { get { return (_measureSize); } set { _measureSize = value; } } /// /// Returns definition's layout time type sensitive preferred size. /// /// /// Returned value is guaranteed to be true preferred size. /// internal double PreferredSize { get { double preferredSize = MinSize; if (_sizeType != Grid.LayoutTimeSizeType.Auto && preferredSize < _measureSize) { preferredSize = _measureSize; } return (preferredSize); } } /// /// Returns or sets size cache for the definition. /// internal double SizeCache { get { return (_sizeCache); } set { _sizeCache = value; } } /// /// Returns min size. /// internal double MinSize { get { double minSize = _minSize; if (UseSharedMinimum && _sharedState != null && minSize < _sharedState.MinSize) { minSize = _sharedState.MinSize; } return (minSize); } } /// /// Returns min size, always taking into account shared state. /// internal double MinSizeForArrange { get { double minSize = _minSize; if (_sharedState != null && (UseSharedMinimum || !LayoutWasUpdated) && minSize < _sharedState.MinSize) { minSize = _sharedState.MinSize; } return (minSize); } } /// /// Offset. /// internal double FinalOffset { get { return _offset; } set { _offset = value; } } /// /// Internal helper to access up-to-date UserSize property value. /// internal abstract GridLength UserSizeValueCache { get; } /// /// Internal helper to access up-to-date UserMinSize property value. /// internal abstract double UserMinSizeValueCache { get; } /// /// Internal helper to access up-to-date UserMaxSize property value. /// internal abstract double UserMaxSizeValueCache { get; } internal Grid Parent { get; set; } /// /// 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); } private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { DefinitionBase definition = (DefinitionBase)d; if (definition.Parent != null) { string sharedSizeGroupId = (string)e.NewValue; if (definition._sharedState != null) { // if definition is already registered AND shared size group id is changing, // then un-register the definition from the current shared size state object. definition._sharedState.RemoveMember(definition); definition._sharedState = null; } if ((definition._sharedState == null) && (sharedSizeGroupId != null)) { SharedSizeScope privateSharedSizeScope = definition.PrivateSharedSizeScope; if (privateSharedSizeScope != null) { // if definition is not registered and both: shared size group id AND private shared scope // are available, then register definition. definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); definition._sharedState.AddMember(definition); } } } } /// /// Verifies that Shared Size Group Property string /// a) not empty. /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// private static string SharedSizeGroupPropertyValueValid(Control _, string value) { Contract.Requires(value != null); string id = (string)value; if (id != string.Empty) { int i = -1; while (++i < id.Length) { bool isDigit = Char.IsDigit(id[i]); if ((i == 0 && isDigit) || !(isDigit || Char.IsLetter(id[i]) || '_' == id[i])) { break; } } if (i == id.Length) { return value; } } throw new ArgumentException("Invalid SharedSizeGroup string."); } /// /// OnPrivateSharedSizeScopePropertyChanged is called when new scope enters or /// existing scope just left. In both cases if the DefinitionBase object is already registered /// in SharedSizeState, it should un-register and register itself in a new one. /// private static void OnPrivateSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { DefinitionBase definition = (DefinitionBase)d; if (definition.Parent != null) { SharedSizeScope privateSharedSizeScope = (SharedSizeScope)e.NewValue; if (definition._sharedState != null) { // if definition is already registered And shared size scope is changing, // then un-register the definition from the current shared size state object. definition._sharedState.RemoveMember(definition); definition._sharedState = null; } if ((definition._sharedState == null) && (privateSharedSizeScope != null)) { string sharedSizeGroup = definition.SharedSizeGroup; if (sharedSizeGroup != null) { // if definition is not registered and both: shared size group id AND private shared scope // are available, then register definition. definition._sharedState = privateSharedSizeScope.EnsureSharedState(definition.SharedSizeGroup); definition._sharedState.AddMember(definition); } } } } /// /// Private getter of shared state collection dynamic property. /// private SharedSizeScope PrivateSharedSizeScope { get { return (SharedSizeScope)GetValue(PrivateSharedSizeScopeProperty); } } /// /// Convenience accessor to UseSharedMinimum flag /// private bool UseSharedMinimum { get { return (CheckFlagsAnd(Flags.UseSharedMinimum)); } set { SetFlags(value, Flags.UseSharedMinimum); } } /// /// Convenience accessor to LayoutWasUpdated flag /// private bool LayoutWasUpdated { get { return (CheckFlagsAnd(Flags.LayoutWasUpdated)); } set { SetFlags(value, Flags.LayoutWasUpdated); } } private Flags _flags; // flags reflecting various aspects of internal state internal int _parentIndex = -1; // this instance's index in parent's children collection private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content" private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) private SharedSizeState _sharedState; // reference to shared state object this instance is registered with [System.Flags] private enum Flags : byte { // // bool flags // UseSharedMinimum = 0x00000020, // when "1", definition will take into account shared state's minimum LayoutWasUpdated = 0x00000040, // set to "1" every time the parent grid is measured } /// /// Collection of shared states objects for a single scope /// internal class SharedSizeScope { /// /// Returns SharedSizeState object for a given group. /// Creates a new StatedState object if necessary. /// internal SharedSizeState EnsureSharedState(string sharedSizeGroup) { // check that sharedSizeGroup is not default Debug.Assert(sharedSizeGroup != null); SharedSizeState sharedState = _registry[sharedSizeGroup] as SharedSizeState; if (sharedState == null) { sharedState = new SharedSizeState(this, sharedSizeGroup); _registry[sharedSizeGroup] = sharedState; } return (sharedState); } /// /// Removes an entry in the registry by the given key. /// internal void Remove(object key) { Debug.Assert(_registry.Contains(key)); _registry.Remove(key); } private Hashtable _registry = new Hashtable(); // storage for shared state objects } /// /// Implementation of per shared group state object /// internal class SharedSizeState { /// /// Default ctor. /// internal SharedSizeState(SharedSizeScope sharedSizeScope, string sharedSizeGroupId) { Debug.Assert(sharedSizeScope != null && sharedSizeGroupId != null); _sharedSizeScope = sharedSizeScope; _sharedSizeGroupId = sharedSizeGroupId; _registry = new List(); _layoutUpdated = new EventHandler(OnLayoutUpdated); _broadcastInvalidation = true; } /// /// Adds / registers a definition instance. /// internal void AddMember(DefinitionBase member) { Debug.Assert(!_registry.Contains(member)); _registry.Add(member); Invalidate(); } /// /// Removes / un-registers a definition instance. /// /// /// If the collection of registered definitions becomes empty /// instantiates self removal from owner's collection. /// internal void RemoveMember(DefinitionBase member) { Invalidate(); _registry.Remove(member); if (_registry.Count == 0) { _sharedSizeScope.Remove(_sharedSizeGroupId); } } /// /// Propogates invalidations for all registered definitions. /// Resets its own state. /// internal void Invalidate() { _userSizeValid = false; if (_broadcastInvalidation) { for (int i = 0, count = _registry.Count; i < count; ++i) { Grid parentGrid = (Grid)(_registry[i].Parent); parentGrid.Invalidate(); } _broadcastInvalidation = false; } } /// /// Makes sure that one and only one layout updated handler is registered for this shared state. /// internal void EnsureDeferredValidation(Control layoutUpdatedHost) { if (_layoutUpdatedHost == null) { _layoutUpdatedHost = layoutUpdatedHost; _layoutUpdatedHost.LayoutUpdated += _layoutUpdated; } } /// /// DefinitionBase's specific code. /// internal double MinSize { get { if (!_userSizeValid) { EnsureUserSizeValid(); } return (_minSize); } } /// /// DefinitionBase's specific code. /// internal GridLength UserSize { get { if (!_userSizeValid) { EnsureUserSizeValid(); } return (_userSize); } } private void EnsureUserSizeValid() { _userSize = new GridLength(1, GridUnitType.Auto); for (int i = 0, count = _registry.Count; i < count; ++i) { Debug.Assert(_userSize.GridUnitType == GridUnitType.Auto || _userSize.GridUnitType == GridUnitType.Pixel); GridLength currentGridLength = _registry[i].UserSizeValueCache; if (currentGridLength.GridUnitType == GridUnitType.Pixel) { if (_userSize.GridUnitType == GridUnitType.Auto) { _userSize = currentGridLength; } else if (_userSize.Value < currentGridLength.Value) { _userSize = currentGridLength; } } } // taking maximum with user size 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. _minSize = _userSize.IsAbsolute ? _userSize.Value : 0.0; _userSizeValid = true; } /// /// OnLayoutUpdated handler. Validates that all participating definitions /// have updated min size value. Forces another layout update cycle if needed. /// private void OnLayoutUpdated(object sender, EventArgs e) { double sharedMinSize = 0; // accumulate min size of all participating definitions for (int i = 0, count = _registry.Count; i < count; ++i) { sharedMinSize = Math.Max(sharedMinSize, _registry[i].MinSize); } bool sharedMinSizeChanged = !MathUtilities.AreClose(_minSize, sharedMinSize); // compare accumulated min size with min sizes of the individual definitions for (int i = 0, count = _registry.Count; i < count; ++i) { DefinitionBase definitionBase = _registry[i]; if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) { // if definition's min size is different, then need to re-measure if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) { Grid parentGrid = (Grid)definitionBase.Parent; parentGrid.InvalidateMeasure(); definitionBase.UseSharedMinimum = true; } else { definitionBase.UseSharedMinimum = false; // if measure is valid then also need to check arrange. // Note: definitionBase.SizeCache is volatile but at this point // it contains up-to-date final size if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) { Grid parentGrid = (Grid)definitionBase.Parent; parentGrid.InvalidateArrange(); } } definitionBase.LayoutWasUpdated = false; } } _minSize = sharedMinSize; _layoutUpdatedHost.LayoutUpdated -= _layoutUpdated; _layoutUpdatedHost = null; _broadcastInvalidation = true; } // the scope this state belongs to private readonly SharedSizeScope _sharedSizeScope; // Id of the shared size group this object is servicing private readonly string _sharedSizeGroupId; // Registry of participating definitions private readonly List _registry; // Instance event handler for layout updated event private readonly EventHandler _layoutUpdated; // Control for which layout updated event handler is registered private Control _layoutUpdatedHost; // "true" when broadcasting of invalidation is needed private bool _broadcastInvalidation; // "true" when _userSize is up to date private bool _userSizeValid; // shared state private GridLength _userSize; // shared state private double _minSize; } /// /// Private shared size scope property holds a collection of shared state objects for the a given shared size scope. /// /// internal static readonly AttachedProperty PrivateSharedSizeScopeProperty = AvaloniaProperty.RegisterAttached( "PrivateSharedSizeScope", defaultValue: null, inherits: true); /// /// Shared size group property marks column / row definition as belonging to a group "Foo" or "Bar". /// /// /// Value of the Shared Size Group Property must satisfy the following rules: /// /// /// String must not be empty. /// /// /// String must consist of letters, digits and underscore ('_') only. /// /// /// String must not start with a digit. /// /// /// public static readonly AttachedProperty SharedSizeGroupProperty = AvaloniaProperty.RegisterAttached( "SharedSizeGroup", validate: SharedSizeGroupPropertyValueValid); /// /// Static ctor. Used for static registration of properties. /// static DefinitionBase() { SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); } /// /// Marks a property on a definition as affecting the parent grid's measurement. /// /// The properties. protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) { void Invalidate(AvaloniaPropertyChangedEventArgs e) { (e.Sender as DefinitionBase)?.Parent?.InvalidateMeasure(); } foreach (var property in properties) { property.Changed.Subscribe(Invalidate); } } } }