diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 804ca1f9b8..6a40f7187d 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -11,6 +11,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index c35b4a7919..4027c5cd63 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -47,7 +47,11 @@ namespace ControlCatalog.NetCore => AppBuilder.Configure() .UsePlatformDetect() .With(new X11PlatformOptions {EnableMultiTouch = true}) - .With(new Win32PlatformOptions {EnableMultitouch = true}) + .With(new Win32PlatformOptions + { + EnableMultitouch = true, + AllowEglInitialization = true + }) .UseSkia() .UseReactiveUI(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs index 05193172be..f94f10f792 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Utilities; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Collections.Generic; using XamlIl.TypeSystem; namespace Avalonia.Build.Tasks @@ -144,6 +145,37 @@ namespace Avalonia.Build.Tasks }); } + + + private static bool MatchThisCall(Collection instructions, int idx) + { + var i = instructions[idx]; + // A "normal" way of passing `this` to a static method: + + // ldarg.0 + // call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object) + + if (i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true)) + return true; + + /* F# way of using `this` in constructor emits a monstrosity like this: + IL_01c7: ldarg.0 + IL_01c8: ldfld class [FSharp.Core]Microsoft.FSharp.Core.FSharpRef`1 FVim.Cursor::this + IL_01cd: call instance !0 class [FSharp.Core]Microsoft.FSharp.Core.FSharpRef`1::get_contents() + IL_01d2: call !!0 [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions::CheckThis(!!0) + IL_01d7: call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object) + + We check for the previous call to be Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions::CheckThis + since it actually returns `this` + */ + if (i.OpCode == OpCodes.Call + && i.Operand is GenericInstanceMethod gim + && gim.Name == "CheckThis" + && gim.DeclaringType.FullName == "Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions") + return true; + + return false; + } } } diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index df840464c1..c54d8e19a1 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -234,8 +234,7 @@ namespace Avalonia.Build.Tasks var i = method.Body.Instructions; for (var c = 1; c < i.Count; c++) { - if (i[c - 1].OpCode == OpCodes.Ldarg_0 - && i[c].OpCode == OpCodes.Call) + if (i[c].OpCode == OpCodes.Call) { var op = i[c].Operand as MethodReference; @@ -254,8 +253,11 @@ namespace Avalonia.Build.Tasks && op.Parameters[0].ParameterType.FullName == "System.Object" && op.DeclaringType.FullName == "Avalonia.Markup.Xaml.AvaloniaXamlLoader") { - i[c].Operand = trampoline; - foundXamlLoader = true; + if (MatchThisCall(i, c - 1)) + { + i[c].Operand = trampoline; + foundXamlLoader = true; + } } } } diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index cc9e6b7444..70f26288af 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -252,7 +252,6 @@ namespace Avalonia.Controls if (e.MouseButton == MouseButton.Left) { - e.Device.Capture(this); IsPressed = true; e.Handled = true; @@ -270,7 +269,6 @@ namespace Avalonia.Controls if (IsPressed && e.MouseButton == MouseButton.Left) { - e.Device.Capture(null); IsPressed = false; e.Handled = true; @@ -282,6 +280,11 @@ namespace Avalonia.Controls } } + protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + IsPressed = false; + } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) { base.UpdateDataValidation(property, status); diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index d316881a05..e3d2489241 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -55,11 +55,7 @@ namespace Avalonia.Controls /// /// Gets the actual calculated width of the column. /// - public double ActualWidth - { - get; - internal set; - } + public double ActualWidth => Parent?.GetFinalColumnDefinitionWidth(Index) ?? 0d; /// /// Gets or sets the maximum width of the column in DIPs. @@ -87,5 +83,9 @@ namespace Avalonia.Controls get { return GetValue(WidthProperty); } set { SetValue(WidthProperty, value); } } + + internal override GridLength UserSizeValueCache => this.Width; + internal override double UserMinSizeValueCache => this.MinWidth; + internal override double UserMaxSizeValueCache => this.MaxWidth; } } diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/ColumnDefinitions.cs index ecfe6027ac..4f5bbf3bc3 100644 --- a/src/Avalonia.Controls/ColumnDefinitions.cs +++ b/src/Avalonia.Controls/ColumnDefinitions.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; @@ -9,7 +11,7 @@ namespace Avalonia.Controls /// /// A collection of s. /// - public class ColumnDefinitions : AvaloniaList + public class ColumnDefinitions : DefinitionList { /// /// Initializes a new instance of the class. @@ -17,6 +19,7 @@ namespace Avalonia.Controls public ColumnDefinitions() { ResetBehavior = ResetBehavior.Remove; + CollectionChanged += OnCollectionChanged; } /// diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 5726356830..8899c38bf9 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -1,26 +1,947 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// 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 { /// - /// Base class for and . + /// DefinitionBase provides core functionality used internally by Grid + /// and ColumnDefinitionCollection / RowDefinitionCollection /// - public class DefinitionBase : AvaloniaObject + public abstract class DefinitionBase : AvaloniaObject { - /// - /// Defines the property. - /// - public static readonly StyledProperty SharedSizeGroupProperty = - AvaloniaProperty.Register(nameof(SharedSizeGroup), inherits: true); + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + /* internal DefinitionBase(bool isColumnDefinition) + { + _isColumnDefinition = isColumnDefinition; + _parentIndex = -1; + }*/ + + #endregion Constructors + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + #region Public Properties /// - /// Gets or sets the name of the shared size group of the column or row. + /// SharedSizeGroup property. /// public string SharedSizeGroup { - get { return GetValue(SharedSizeGroupProperty); } + get { return (string) GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + /// + /// 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); + } + } + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.PropertyType == typeof(GridLength) + || e.Property.PropertyType == typeof(double)) + OnUserSizePropertyChanged(e); + + base.OnPropertyChanged(e); + } + + /// + /// Callback to notify about exitting model tree. + /// + internal void OnExitParentTree() + { + _offset = 0; + if (_sharedState != null) + { + _sharedState.RemoveMember(this); + _sharedState = null; + } + } + + /// + /// 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 needs to be internal to be accessable from derived classes. + /// + internal void OnUserSizePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (InParentLogicalTree) + { + if (_sharedState != null) + { + _sharedState.Invalidate(); + } + else + { + if (((GridLength)e.OldValue).GridUnitType != ((GridLength)e.NewValue).GridUnitType) + { + Parent.Invalidate(); + } + else + { + Parent.InvalidateMeasure(); + } + } + } + } + /// + /// + /// + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static bool IsUserSizePropertyValueValid(object value) + { + return (((GridLength)value).Value >= 0); + } + + /// + /// + /// + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static void OnUserMinSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + DefinitionBase definition = (DefinitionBase) d; + + if (definition.InParentLogicalTree) + { + Grid parentGrid = (Grid) definition.Parent; + parentGrid.InvalidateMeasure(); + } + } + + /// + /// + /// + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static bool IsUserMinSizePropertyValueValid(object value) + { + double v = (double)value; + return (!double.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); + } + + /// + /// + /// + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static void OnUserMaxSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + DefinitionBase definition = (DefinitionBase) d; + + if (definition.InParentLogicalTree) + { + Grid parentGrid = (Grid) definition.Parent; + parentGrid.InvalidateMeasure(); + } + } + + /// + /// + /// + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static bool IsUserMaxSizePropertyValueValid(object value) + { + double v = (double)value; + return (!double.IsNaN(v) && v >= 0.0d); + } + + /// + /// + /// + /// + /// This method reflects Grid.SharedScopeProperty state by setting / clearing + /// dynamic property PrivateSharedSizeScopeProperty. Value of PrivateSharedSizeScopeProperty + /// is a collection of SharedSizeState objects for the scope. + /// Also PrivateSharedSizeScopeProperty is FrameworkPropertyMetadataOptions.Inherits property. So that all children + /// elements belonging to a certain scope can easily access SharedSizeState collection. As well + /// as been norified about enter / exit a scope. + /// + internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + // is it possible to optimize here something like this: + // if ((bool)d.GetValue(Grid.IsSharedSizeScopeProperty) == (d.GetLocalValue(PrivateSharedSizeScopeProperty) != null) + // { /* do nothing */ } + if ((bool) e.NewValue) + { + SharedSizeScope sharedStatesCollection = new SharedSizeScope(); + d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); + } + else + { + d.ClearValue(PrivateSharedSizeScopeProperty); + } + } + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + /// + /// 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; } + + /// + /// Protected. Returns true if this DefinitionBase instance is in parent's logical tree. + /// + internal bool InParentLogicalTree + { + get { return (_parentIndex != -1); } + } + + internal Grid Parent { get; set; } + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + #region Private Methods + + /// + /// 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.InParentLogicalTree) + { + 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.InParentLogicalTree) + { + 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); + } + } + } + } + + #endregion Private Methods + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + #region Private Properties + + /// + /// 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); } + } + + #endregion Private Properties + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + private readonly bool _isColumnDefinition; // when "true", this is a ColumnDefinition; when "false" this is a RowDefinition (faster than a type check) + 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 + + internal const bool ThisIsColumnDefinition = true; + internal const bool ThisIsRowDefinition = false; + + #endregion Private Fields + + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ + + #region Private Structures Classes + + [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; + } + + private readonly SharedSizeScope _sharedSizeScope; // the scope this state belongs to + private readonly string _sharedSizeGroupId; // Id of the shared size group this object is servicing + private readonly List _registry; // registry of participating definitions + private readonly EventHandler _layoutUpdated; // instance event handler for layout updated event + private Control _layoutUpdatedHost; // Control for which layout updated event handler is registered + private bool _broadcastInvalidation; // "true" when broadcasting of invalidation is needed + private bool _userSizeValid; // "true" when _userSize is up to date + private GridLength _userSize; // shared state + private double _minSize; // shared state + } + + #endregion Private Structures Classes + + //------------------------------------------------------ + // + // Properties + // + //------------------------------------------------------ + + #region Properties + + /// + /// 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); + } + + #endregion Properties } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/DefinitionList.cs b/src/Avalonia.Controls/DefinitionList.cs new file mode 100644 index 0000000000..b36ca9ce8a --- /dev/null +++ b/src/Avalonia.Controls/DefinitionList.cs @@ -0,0 +1,55 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Collections; + +namespace Avalonia.Controls +{ + public abstract class DefinitionList : AvaloniaList where T : DefinitionBase + { + internal bool IsDirty = true; + private Grid _parent; + + internal Grid Parent + { + get => _parent; + set => SetParent(value); + } + + + private void SetParent(Grid value) + { + _parent = value; + + foreach (var pair in this.Select((definitions, index) => (definitions, index))) + { + pair.definitions.Parent = value; + pair.definitions.Index = pair.index; + } + } + + internal void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + foreach (var nI in this.Select((d, i) => (d, i))) + nI.d._parentIndex = nI.i; + + foreach (var nD in e.NewItems?.Cast() + ?? Enumerable.Empty()) + { + nD.Parent = this.Parent; + nD.OnEnterParentTree(); + } + + foreach (var oD in e.OldItems?.Cast() + ?? Enumerable.Empty()) + { + oD.OnExitParentTree(); + } + + IsDirty = true; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 90a27d0b31..fc61c409f0 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1,601 +1,4066 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// 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 System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; +using System.Threading; +using Avalonia; using Avalonia.Collections; -using Avalonia.Controls.Utils; +using Avalonia.Media; +using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { /// - /// Lays out child controls according to a grid. + /// Grid /// public class Grid : Panel { + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + static Grid() + { + IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); + ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + + ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + } + /// - /// Defines the Column attached property. + /// Default constructor. /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached( - "Column", - validate: ValidateColumn); + public Grid() + { + } + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + /// - /// Defines the ColumnSpan attached property. + /// /// - public static readonly AttachedProperty ColumnSpanProperty = - AvaloniaProperty.RegisterAttached("ColumnSpan", 1); + /* 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)); + } + } */ /// - /// Defines the Row attached property. + /// Helper for setting Column property on a Control. /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached( - "Row", - validate: ValidateRow); + /// Control to set Column property on. + /// Column property value. + public static void SetColumn(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(ColumnProperty, value); + } /// - /// Defines the RowSpan attached property. + /// Helper for reading Column property from a Control. /// - public static readonly AttachedProperty RowSpanProperty = - AvaloniaProperty.RegisterAttached("RowSpan", 1); + /// Control to read Column property from. + /// Column property value. + public static int GetColumn(Control element) + { + Contract.Requires(element != null); + return element.GetValue(ColumnProperty); + } - public static readonly AttachedProperty IsSharedSizeScopeProperty = - AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + /// + /// Helper for setting Row property on a Control. + /// + /// Control to set Row property on. + /// Row property value. + public static void SetRow(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(RowProperty, value); + } - protected override void OnMeasureInvalidated() + /// + /// Helper for reading Row property from a Control. + /// + /// Control to read Row property from. + /// Row property value. + public static int GetRow(Control element) { - base.OnMeasureInvalidated(); - _sharedSizeHost?.InvalidateMeasure(this); + Contract.Requires(element != null); + return element.GetValue(RowProperty); } - private SharedSizeScopeHost _sharedSizeHost; + /// + /// Helper for setting ColumnSpan property on a Control. + /// + /// Control to set ColumnSpan property on. + /// ColumnSpan property value. + public static void SetColumnSpan(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(ColumnSpanProperty, value); + } /// - /// Defines the SharedSizeScopeHost private property. - /// The ampersands are used to make accessing the property via xaml inconvenient. + /// Helper for reading ColumnSpan property from a Control. /// - internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); + /// Control to read ColumnSpan property from. + /// ColumnSpan property value. + public static int GetColumnSpan(Control element) + { + Contract.Requires(element != null); + return element.GetValue(ColumnSpanProperty); + } - private ColumnDefinitions _columnDefinitions; + /// + /// Helper for setting RowSpan property on a Control. + /// + /// Control to set RowSpan property on. + /// RowSpan property value. + public static void SetRowSpan(Control element, int value) + { + Contract.Requires(element != null); + element.SetValue(RowSpanProperty, value); + } - private RowDefinitions _rowDefinitions; + /// + /// Helper for reading RowSpan property from a Control. + /// + /// Control to read RowSpan property from. + /// RowSpan property value. + public static int GetRowSpan(Control element) + { + Contract.Requires(element != null); + return element.GetValue(RowSpanProperty); + } - static Grid() + /// + /// Helper for setting IsSharedSizeScope property on a Control. + /// + /// Control to set IsSharedSizeScope property on. + /// IsSharedSizeScope property value. + public static void SetIsSharedSizeScope(Control element, bool value) { - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); + Contract.Requires(element != null); + element.SetValue(IsSharedSizeScopeProperty, value); } - public Grid() + /// + /// Helper for reading IsSharedSizeScope property from a Control. + /// + /// Control to read IsSharedSizeScope property from. + /// IsSharedSizeScope property value. + public static bool GetIsSharedSizeScope(Control element) + { + Contract.Requires(element != null); + return element.GetValue(IsSharedSizeScopeProperty); + } + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + /// + /// ShowGridLines property. + /// + public bool ShowGridLines { - this.AttachedToVisualTree += Grid_AttachedToVisualTree; - this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; + get { return GetValue(ShowGridLinesProperty); } + set { SetValue(ShowGridLinesProperty, value); } } /// - /// Gets or sets the columns definitions for the grid. + /// Returns a ColumnDefinitions of column definitions. /// public ColumnDefinitions ColumnDefinitions { get { - if (_columnDefinitions == null) + if (_data == null) { _data = new ExtendedData(); } + if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitions() { Parent = this }; } + + return (_data.ColumnDefinitions); + } + set + { + if (_data == null) { _data = new ExtendedData(); } + _data.ColumnDefinitions = value; + _data.ColumnDefinitions.Parent = this; + } + } + + /// + /// Returns a RowDefinitions of row definitions. + /// + public RowDefinitions RowDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitions() { Parent = this }; } + + return (_data.RowDefinitions); + } + set + { + if (_data == null) { _data = new ExtendedData(); } + _data.RowDefinitions = value; + _data.RowDefinitions.Parent = this; + } + } + + //------------------------------------------------------ + // + // 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) { - ColumnDefinitions = new ColumnDefinitions(); + 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 + { + + + ListenToNotifications = true; + MeasureOverrideInProgress = true; - return _columnDefinitions; + if (extData == null) + { + gridDesiredSize = new Size(); + var children = this.Children; + + for (int i = 0, count = children.Count; i < count; ++i) + { + var child = children[i]; + if (child != null) + { + child.Measure(constraint); + gridDesiredSize = new Size(Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); + } + } + } + else + { + { + bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); + bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); + + // Clear index information and rounding errors + if (RowDefinitionsDirty || ColumnDefinitionsDirty) + { + if (_definitionIndices != null) + { + Array.Clear(_definitionIndices, 0, _definitionIndices.Length); + _definitionIndices = null; + } + + if (UseLayoutRounding) + { + if (_roundingErrors != null) + { + Array.Clear(_roundingErrors, 0, _roundingErrors.Length); + _roundingErrors = null; + } + } + } + + ValidateDefinitionsUStructure(); + ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); + + ValidateDefinitionsVStructure(); + ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); + + CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); + + SizeToContentU = sizeToContentU; + SizeToContentV = sizeToContentV; + } + + ValidateCells(); + + Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 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); + + + gridDesiredSize = new Size( + CalculateDesiredSize(DefinitionsU), + CalculateDesiredSize(DefinitionsV)); + + } + } + finally + { + MeasureOverrideInProgress = false; + } - set + return (gridDesiredSize); + } + + /// + /// Content arrangement. + /// + /// Arrange size + protected override Size ArrangeOverride(Size arrangeSize) + { + try + { + + + ArrangeOverrideInProgress = true; + + if (_data == null) + { + var children = this.Children; + + for (int i = 0, count = children.Count; i < count; ++i) + { + var child = children[i]; + if (child != null) + { + child.Arrange(new Rect(arrangeSize)); + } + } + } + else + { + Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0); + + + + SetFinalSize(DefinitionsU, arrangeSize.Width, true); + SetFinalSize(DefinitionsV, arrangeSize.Height, false); + + + + var children = this.Children; + + for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) + { + var 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) ); + + + cell.Arrange(cellRect); + + } + + // update render bound on grid lines renderer visual + var gridLinesRenderer = EnsureGridLinesRenderer(); + gridLinesRenderer?.UpdateRenderBounds(arrangeSize); + } + } + finally + { + SetValid(); + ArrangeOverrideInProgress = false; + + } + return (arrangeSize); + } + + /// + /// + /// + /*protected internal override void OnVisualChildrenChanged( + AvaloniaObject visualAdded, + AvaloniaObject visualRemoved) + { + CellsStructureDirty = true; + + base.OnVisualChildrenChanged(visualAdded, visualRemoved); + }*/ + + //------------------------------------------------------ + // + // 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; + + Contract.Requires(_data != null); + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionsDirty) + { + IReadOnlyList definitions = DefinitionsU; + value = definitions[(columnIndex + 1) % definitions.Count].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; + + Contract.Requires(_data != null); + + // actual value calculations require structure to be up-to-date + if (!RowDefinitionsDirty) + { + IReadOnlyList definitions = DefinitionsV; + value = definitions[(rowIndex + 1) % definitions.Count].FinalOffset; + if (rowIndex != 0) { value -= definitions[rowIndex].FinalOffset; } + } + return (value); + } + + //------------------------------------------------------ + // + // 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 ColumnDefinitionsDirty + { + get => ColumnDefinitions?.IsDirty ?? false; + set => ColumnDefinitions.IsDirty = value; + } + + /// + /// Convenience accessor to ValidDefinitionsVStructure bit flag. + /// + internal bool RowDefinitionsDirty + { + get => RowDefinitions?.IsDirty ?? false; + set => RowDefinitions.IsDirty = value; + } + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + /// + /// Lays out cells according to rows and columns, and creates lookup grids. + /// + private void ValidateCells() + { + + + if (CellsStructureDirty) + { + ValidateCellsCore(); + CellsStructureDirty = false; + } + + + } + + /// + /// ValidateCellsCore + /// + private void ValidateCellsCore() + { + var children = this.Children; + 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) + { + var 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((Control)child), DefinitionsU.Count - 1); + // clamp to value < number_of_rows + // row >= 0 is guaranteed by property value validation callback + cell.RowIndex = Math.Min(GetRow((Control)child), DefinitionsV.Count - 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((Control)child), DefinitionsU.Count - 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((Control)child), DefinitionsV.Count - cell.RowIndex); + + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Count); + Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Count); + + // + // 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() + { + if (ColumnDefinitionsDirty) + { + ExtendedData extData = ExtData; + + if (extData.ColumnDefinitions == null) + { + if (extData.DefinitionsU == null) + { + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() { Parent = this } }; + } + } + else + { + if (extData.ColumnDefinitions.Count == 0) + { + // if column definitions collection is empty + // mockup array with one column + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() { Parent = this } }; + } + else + { + extData.DefinitionsU = extData.ColumnDefinitions; + } + } + + ColumnDefinitionsDirty = false; + } + + Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Count > 0); + } + + /// + /// 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() + { + if (RowDefinitionsDirty) + { + ExtendedData extData = ExtData; + + if (extData.RowDefinitions == null) + { + if (extData.DefinitionsV == null) + { + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() { Parent = this } }; + } + } + else + { + if (extData.RowDefinitions.Count == 0) + { + // if row definitions collection is empty + // mockup array with one row + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() { Parent = this } }; + } + else + { + extData.DefinitionsV = extData.RowDefinitions; + } + } + + RowDefinitionsDirty = false; + } + + Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Count > 0); + } + + /// + /// 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( + IReadOnlyList definitions, + bool treatStarAsAuto) + { + for (int i = 0; i < definitions.Count; ++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.Count] : new double[DefinitionsU.Count]; + + 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; + } + + var children = this.Children; + Hashtable spanStore = null; + bool ignoreDesiredSizeV = forceInfinityV; + + int i = cellsHead; + do + { + double oldWidth = children[i].DesiredSize.Width; + + MeasureCell(i, forceInfinityV); + + hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, children[i].DesiredSize.Width); + + if (!ignoreDesiredSizeU) + { + if (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) + { + + + 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); + } + + + var child = this.Children[cell]; + if (child != null) + { + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); + } + + + + } + + + /// + /// Calculates one dimensional measure size for given definitions' range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Calculated measure size. + /// + /// For "Auto" definitions MinWidth is used in place of PreferredSize. + /// + private double GetMeasureSizeForRange( + IReadOnlyList definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Count); + + 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( + IReadOnlyList definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Count); + + 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( + IReadOnlyList definitions, + int start, + int count, + double requestedSize, + double percentReferenceSize) + { + Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count); + + // 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) + && !double.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !double.IsNaN(sizeToDistribute) + && sizeToDistribute > 0 ); + + for (int i = 0; i < count; ++i) + { + double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + } + } + else + { + // + // equi-size is greater or equal to maximum of max sizes. + // all definitions receive equalSize as their mim sizes. + // + for (int i = 0; i < count; ++i) + { + tempDefinitions[i].UpdateMinSize(equalSize); + } + } + } + } + } + } + + /// + /// 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( + IReadOnlyList 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( + IReadOnlyList definitions, + double availableSize) + { + DefinitionBase[] tempDefinitions = TempDefinitions; + int starDefinitionsCount = 0; + double takenSize = 0; + + for (int i = 0; i < definitions.Count; ++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( + IReadOnlyList definitions, + double availableSize) + { + int defCount = definitions.Count; + 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( + IReadOnlyList definitions) + { + double desiredSize = 0; + + for (int i = 0; i < definitions.Count; ++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( + IReadOnlyList 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( + IReadOnlyList definitions, + double finalSize, + bool columns) + { + int starDefinitionsCount = 0; // traverses form the first entry up + int nonStarIndex = definitions.Count; // 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; + dpi = (VisualRoot as Layout.ILayoutRoot)?.LayoutScaling ?? 1.0; + roundingErrors = RoundingErrors; + } + + for (int i = 0; i < definitions.Count; ++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 = MathUtilities.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 = MathUtilities.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 = MathUtilities.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.Count, distributionOrderIndexComparer); + double sizeToDistribute = finalSize - allPreferredArrangeSize; + + for (int i = 0; i < definitions.Count; ++i) + { + int definitionIndex = definitionIndices[i]; + double final = definitions[definitionIndex].SizeCache + (sizeToDistribute / (definitions.Count - i)); + double finalOld = final; + final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); + final = Math.Min(final, definitions[definitionIndex].SizeCache); + + if (useLayoutRounding) + { + roundingErrors[definitionIndex] = final; + final = MathUtilities.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.Count; ++i) + { + roundingErrors[i] = roundingErrors[i] - definitions[i].SizeCache; + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Count, roundingErrorIndexComparer); + double adjustedSize = allPreferredArrangeSize; + double dpiIncrement = MathUtilities.RoundLayoutValue(1.0, dpi); + + if (allPreferredArrangeSize > finalSize) + { + int i = definitions.Count - 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.Count) + { + 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.Count; ++i) + { + definitions[(i + 1) % definitions.Count].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( + IReadOnlyList definitions, + double finalSize, + bool columns) + { + int defCount = definitions.Count; + 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; + var dpi = (VisualRoot as Layout.ILayoutRoot)?.LayoutScaling ?? 1.0; + double[] roundingErrors = RoundingErrors; + double roundedTakenSize = 0.0; + + // round each of the allocated sizes, keeping track of the deltas + for (int i = 0; i < definitions.Count; ++i) + { + DefinitionBase def = definitions[i]; + double roundedSize = MathUtilities.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.Count; ++i) + { + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Count, roundingErrorIndexComparer); + double adjustedSize = roundedTakenSize; + double dpiIncrement = 1.0/dpi; + + if (roundedTakenSize > finalSize) + { + int i = definitions.Count - 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.Count) + { + 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.Count; ++i) + { + definitions[(i + 1) % definitions.Count].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( + IReadOnlyList 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.Count, DefinitionsV.Count)); + extData.TempDefinitions = null; + } + } + } + + /// + /// Returns true if ColumnDefinitions collection is not empty + /// + public bool ShouldSerializeColumnDefinitions() + { + ExtendedData extData = ExtData; + return ( extData != null + && extData.ColumnDefinitions != null + && extData.ColumnDefinitions.Count > 0 ); + } + + /// + /// Returns true if RowDefinitions collection is not empty + /// + 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.VisualChildren.Add(_gridLinesRenderer); + } + + if ((!ShowGridLines) && (_gridLinesRenderer != null)) + { + this.VisualChildren.Add(_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(AvaloniaObject d, AvaloniaPropertyChangedEventArgs 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(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + Visual child = d as Visual; + + if (child != null) + { + Grid grid = child.GetVisualParent(); + if ( grid != null + && grid.ExtData != null + && grid.ListenToNotifications ) + { + grid.CellsStructureDirty = true; + } + } + } + + /* /// + /// + /// + 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); + } + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + /// + /// Private version returning array of column definitions. + /// + private IReadOnlyList DefinitionsU + { + get { return (ExtData.DefinitionsU); } + } + + /// + /// Private version returning array of row definitions. + /// + private IReadOnlyList 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.Count, DefinitionsV.Count) * 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 { - if (_columnDefinitions != null) + int requiredLength = Math.Max(Math.Max(DefinitionsU.Count, DefinitionsV.Count), 1) * 2; + + if (_definitionIndices == null || _definitionIndices.Length < requiredLength) { - throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); + _definitionIndices = new int[requiredLength]; } - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + return _definitionIndices; } } /// - /// Gets or sets the row definitions for the grid. + /// Helper accessor to rounding errors. /// - public RowDefinitions RowDefinitions + private double[] RoundingErrors { get { - if (_rowDefinitions == null) + int requiredLength = Math.Max(DefinitionsU.Count, DefinitionsV.Count); + + if (_roundingErrors == null && requiredLength == 0) { - RowDefinitions = new RowDefinitions(); + _roundingErrors = new double[1]; } - - return _rowDefinitions; - } - - set - { - if (_rowDefinitions != null) + else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) { - throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); + _roundingErrors = new double[requiredLength]; } - - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + return _roundingErrors; } } /// - /// Gets the value of the Column attached property for a control. + /// Private version returning array of cells. /// - /// The control. - /// The control's column. - public static int GetColumn(AvaloniaObject element) + private CellCache[] PrivateCells { - return element.GetValue(ColumnProperty); + get { return (ExtData.CellCachesCollection); } } /// - /// Gets the value of the ColumnSpan attached property for a control. + /// Convenience accessor to ValidCellsStructure bit flag. /// - /// The control. - /// The control's column span. - public static int GetColumnSpan(AvaloniaObject element) + private bool CellsStructureDirty { - return element.GetValue(ColumnSpanProperty); + get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } + set { SetFlags(!value, Flags.ValidCellsStructure); } } /// - /// Gets the value of the Row attached property for a control. + /// Convenience accessor to ListenToNotifications bit flag. /// - /// The control. - /// The control's row. - public static int GetRow(AvaloniaObject element) + private bool ListenToNotifications { - return element.GetValue(RowProperty); + get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } + set { SetFlags(value, Flags.ListenToNotifications); } } /// - /// Gets the value of the RowSpan attached property for a control. + /// Convenience accessor to SizeToContentU bit flag. /// - /// The control. - /// The control's row span. - public static int GetRowSpan(AvaloniaObject element) + private bool SizeToContentU { - return element.GetValue(RowSpanProperty); + 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); } + } /// - /// Gets the value of the IsSharedSizeScope attached property for a control. + /// Convenience accessor to HasStarCellsV bit flag. /// - /// The control. - /// The control's IsSharedSizeScope value. - public static bool GetIsSharedSizeScope(AvaloniaObject element) + private bool HasStarCellsV { - return element.GetValue(IsSharedSizeScopeProperty); + get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } + set { SetFlags(value, Flags.HasStarCellsV); } } /// - /// Sets the value of the Column attached property for a control. + /// Convenience accessor to HasGroup3CellsInAutoRows bit flag. /// - /// The control. - /// The column value. - public static void SetColumn(AvaloniaObject element, int value) + private bool HasGroup3CellsInAutoRows { - element.SetValue(ColumnProperty, value); + get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } + set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } /// - /// Sets the value of the ColumnSpan attached property for a control. + /// fp version of d == 0. /// - /// The control. - /// The column span value. - public static void SetColumnSpan(AvaloniaObject element, int value) + /// Value to check. + /// true if d == 0. + private static bool _IsZero(double d) { - element.SetValue(ColumnSpanProperty, value); + return (Math.Abs(d) < c_epsilon); } /// - /// Sets the value of the Row attached property for a control. + /// fp version of d1 == d2 /// - /// The control. - /// The row value. - public static void SetRow(AvaloniaObject element, int value) + /// First value to compare + /// Second value to compare + /// true if d1 == d2 + private static bool _AreClose(double d1, double d2) { - element.SetValue(RowProperty, value); + return (Math.Abs(d1 - d2) < c_epsilon); } /// - /// Sets the value of the RowSpan attached property for a control. + /// Returns reference to extended data bag. /// - /// The control. - /// The row span value. - public static void SetRowSpan(AvaloniaObject element, int value) + private ExtendedData ExtData { - element.SetValue(RowSpanProperty, value); + get { return (_data); } } /// - /// Sets the value of IsSharedSizeScope property for a control. + /// Returns *-weight, adjusted for scale computed during Phase 1 /// - /// The control. - /// The IsSharedSizeScope value. - public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) + static double StarWeight(DefinitionBase def, double scale) { - element.SetValue(IsSharedSizeScopeProperty, value); + 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; + } } + //------------------------------------------------------ + // + // 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; + + //------------------------------------------------------ + // + // 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(); + + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ + /// - /// Gets the result of the last column measurement. - /// Use this result to reduce the arrange calculation. + /// Extended data instantiated on demand, when grid handles non-trivial case. /// - private GridLayout.MeasureResult _columnMeasureCache; + private class ExtendedData + { + internal ColumnDefinitions ColumnDefinitions; // collection of column definitions (logical tree support) + internal RowDefinitions RowDefinitions; // collection of row definitions (logical tree support) + internal IReadOnlyList DefinitionsU; // collection of column definitions used during calc + internal IReadOnlyList 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) + } /// - /// Gets the result of the last row measurement. - /// Use this result to reduce the arrange calculation. + /// Grid validity / property caches dirtiness flags /// - private GridLayout.MeasureResult _rowMeasureCache; + [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 + } + + //------------------------------------------------------ + // + // Properties + // + //------------------------------------------------------ /// - /// Gets the row layout as of the last measure. + /// 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. /// - private GridLayout _rowLayoutCache; + public static readonly StyledProperty ShowGridLinesProperty = + AvaloniaProperty.Register(nameof(ShowGridLines)); /// - /// Gets the column layout as of the last measure. + /// 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. /// - private GridLayout _columnLayoutCache; + /// + /// 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. + /// + public static readonly AttachedProperty ColumnProperty = + AvaloniaProperty.RegisterAttached( + "Column", + defaultValue: 0, + validate: (_, v) => { if (v >= 0) return v; + else throw new ArgumentException("Invalid Grid.Column value."); }); /// - /// Measures the grid. + /// 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. + /// /// - /// The available size. - /// The desired size of the control. - protected override Size MeasureOverride(Size constraint) - { - // Situation 1/2: - // If the grid doesn't have any column/row definitions, it behaves like a normal panel. - // GridLayout supports this situation but we handle this separately for performance. + public static readonly AttachedProperty RowProperty = + AvaloniaProperty.RegisterAttached( + "Row", + defaultValue: 0, + validate: (_, v) => { if (v >= 0) return v; + else throw new ArgumentException("Invalid Grid.Row value."); }); - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) - { - var maxWidth = 0.0; - var maxHeight = 0.0; - foreach (var child in Children.OfType()) - { - child.Measure(constraint); - maxWidth = Math.Max(maxWidth, child.DesiredSize.Width); - maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); - } + /// + /// 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. + /// + public static readonly AttachedProperty ColumnSpanProperty = + AvaloniaProperty.RegisterAttached( + "ColumnSpan", + defaultValue: 1, + validate: (_, v) => { if (v >= 1) return v; + else throw new ArgumentException("Invalid Grid.ColumnSpan value."); }); - maxWidth = Math.Min(maxWidth, constraint.Width); - maxHeight = Math.Min(maxHeight, constraint.Height); - return new Size(maxWidth, maxHeight); - } + /// + /// 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. + /// + public static readonly AttachedProperty RowSpanProperty = + AvaloniaProperty.RegisterAttached( + "RowSpan", + defaultValue: 1, + validate: (_, v) => { if (v >= 1) return v; + else throw new ArgumentException("Invalid Grid.RowSpan value."); }); + + /// + /// IsSharedSizeScope property marks scoping element for shared size. + /// + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached( + "IsSharedSizeScope"); - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. + //------------------------------------------------------ + // + // Internal Structures / Classes + // + //------------------------------------------------------ - var measureCache = new Dictionary(); - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - // Note: If a child stays in a * or Auto column/row, use constraint to measure it. - columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); - rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } - // Calculate measurement. - var columnResult = columnLayout.Measure(constraint.Width); - var rowResult = rowLayout.Measure(constraint.Height); + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ - // Use the results of the measurement to measure the rest of the children. - foreach (var child in Children.OfType()) - { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); - var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); + /// + /// 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); } } + } - MeasureOnce(child, new Size(width, height)); + /// + /// 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; } - // Cache the measure result and return the desired size. - _columnMeasureCache = columnResult; - _rowMeasureCache = rowResult; - _rowLayoutCache = rowLayout; - _columnLayoutCache = columnLayout; + /// + /// + /// + public override int GetHashCode() + { + int hash = (_start ^ (_count << 2)); + + if (_u) hash &= 0x7ffffff; + else hash |= 0x8000000; + + return (hash); + } - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + /// + /// + /// + public override bool Equals(object obj) { - _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + SpanKey sk = obj as SpanKey; + return ( sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u ); } - return new Size(columnResult.DesiredLength, rowResult.DesiredLength); + /// + /// 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; + } - // Measure each child only once. - // If a child has been measured, it will just return the desired size. - Size MeasureOnce(Control child, Size size) + /// + /// SpanPreferredDistributionOrderComparer. + /// + private class SpanPreferredDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) { - if (measureCache.TryGetValue(child, out var desiredSize)) + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) { - return desiredSize; + 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); + } + } } - child.Measure(size); - desiredSize = child.DesiredSize; - measureCache[child] = desiredSize; - return desiredSize; + return result; } } /// - /// Arranges the grid's children. + /// SpanMaxDistributionOrderComparer. /// - /// The size allocated to the control. - /// The space taken. - protected override Size ArrangeOverride(Size finalSize) + private class SpanMaxDistributionOrderComparer : IComparer { - // Situation 1/2: - // If the grid doesn't have any column/row definitions, it behaves like a normal panel. - // GridLayout supports this situation but we handle this separately for performance. - - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + public int Compare(object x, object y) { - foreach (var child in Children.OfType()) + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) { - child.Arrange(new Rect(finalSize)); + 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 finalSize; + return result; } + } - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. + /// + /// StarDistributionOrderComparer. + /// + private class StarDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = _columnLayoutCache; - var rowLayout = _rowLayoutCache; + int result; - var rowCache = _rowMeasureCache; - var columnCache = _columnMeasureCache; + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { - (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); - - rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); - columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); + return result; } + } - // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); - var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); - // Arrange the children. - foreach (var child in Children.OfType()) + /// + /// DistributionOrderComparer. + /// + private class DistributionOrderComparer: IComparer + { + public int Compare(object x, object y) { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var x = Enumerable.Range(0, column).Sum(c => columnResult.LengthList[c]); - var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); - var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); - var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); - child.Arrange(new Rect(x, y, width, height)); + 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 IReadOnlyList definitions; - // Assign the actual width. - for (var i = 0; i < ColumnDefinitions.Count; i++) + internal StarDistributionOrderIndexComparer(IReadOnlyList definitions) { - ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; + Contract.Requires(definitions != null); + this.definitions = definitions; } - // Assign the actual height. - for (var i = 0; i < RowDefinitions.Count; i++) + public int Compare(object x, object y) { - RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; - } + 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; - // Return the render size. - return finalSize; + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } } /// - /// Tests whether this grid belongs to a shared size scope. + /// DistributionOrderComparer. /// - /// True if the grid is registered in a shared size scope. - internal bool HasSharedSizeScope() + private class DistributionOrderIndexComparer : IComparer { - return _sharedSizeHost != null; + private readonly IReadOnlyList definitions; + + internal DistributionOrderIndexComparer(IReadOnlyList definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; + double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; + result = xprime.CompareTo(yprime); + } + + return result; + } } /// - /// Called when the SharedSizeScope for a given grid has changed. - /// Unregisters the grid from it's current scope and finds a new one (if any) + /// RoundingErrorIndexComparer. /// - /// - /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. - /// - internal void SharedScopeChanged() + private class RoundingErrorIndexComparer : IComparer { - _sharedSizeHost?.UnegisterGrid(this); - - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + private readonly double[] errors; - if (scope != null) + internal RoundingErrorIndexComparer(double[] errors) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + Contract.Requires(errors != null); + this.errors = errors; } - InvalidateMeasure(); + 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; + } } /// - /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid - /// in it. + /// MinRatioComparer. + /// Sort by w/min (stored in MeasureSize), descending. + /// We query the list from the back, i.e. in ascending order of w/min. /// - /// The source of the event. - /// The event arguments. - private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private class MinRatioComparer : IComparer { - var scope = - new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; - if (_sharedSizeHost != null) - throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + int result; - if (scope != null) - { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; } } /// - /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. + /// MaxRatioComparer. + /// Sort by w/max (stored in SizeCache), ascending. + /// We query the list from the back, i.e. in descending order of w/max. /// - /// The source of the event. - /// The event arguments. - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private class MaxRatioComparer : IComparer { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } + 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; + } + } /// - /// Get the safe column/columnspan and safe row/rowspan. - /// This method ensures that none of the children has a column/row outside the bounds of the definitions. + /// StarWeightComparer. + /// Sort by *-weight (stored in MeasureSize), ascending. /// - [Pure] - private (Dictionary safeColumns, - Dictionary safeRows) GetSafeColumnRows() + private class StarWeightComparer : IComparer { - var columnCount = ColumnDefinitions.Count; - var rowCount = RowDefinitions.Count; - columnCount = columnCount == 0 ? 1 : columnCount; - rowCount = rowCount == 0 ? 1 : rowCount; - var safeColumns = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); - var safeRows = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); - return (safeColumns, safeRows); + 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; + } } /// - /// Gets the safe row/column and rowspan/columnspan for a specified range. - /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. + /// MinRatioIndexComparer. /// - /// The row or column count. - /// The row or column that the user assigned. - /// The rowspan or columnspan that the user assigned. - /// The safe row/column and rowspan/columnspan. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) + private class MinRatioIndexComparer : IComparer { - var index = userIndex; - var span = userSpan; + private readonly IReadOnlyList definitions; - if (index < 0) + internal MinRatioIndexComparer(IReadOnlyList definitions) { - span = index + span; - index = 0; + Contract.Requires(definitions != null); + this.definitions = definitions; } - if (span <= 0) + public int Compare(object x, object y) { - span = 1; + 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; } + } - if (userIndex >= length) + /// + /// MaxRatioIndexComparer. + /// + private class MaxRatioIndexComparer : IComparer + { + private readonly IReadOnlyList definitions; + + internal MaxRatioIndexComparer(IReadOnlyList definitions) { - index = length - 1; - span = 1; + Contract.Requires(definitions != null); + this.definitions = definitions; } - else if (userIndex + userSpan > length) + + public int Compare(object x, object y) { - span = length - userIndex; - } + 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 (index, span); + return result; + } } - private static int ValidateColumn(AvaloniaObject o, int value) + /// + /// MaxRatioIndexComparer. + /// + private class StarWeightIndexComparer : IComparer { - if (value < 0) + private readonly IReadOnlyList definitions; + + internal StarWeightIndexComparer(IReadOnlyList definitions) { - throw new ArgumentException("Invalid Grid.Column value."); + Contract.Requires(definitions != null); + this.definitions = definitions; } - return value; + 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; + } } - private static int ValidateRow(AvaloniaObject o, int value) + /* /// + /// Implementation of a simple enumerator of grid's logical children + /// + private class GridChildrenCollectionEnumeratorSimple : IEnumerator { - if (value < 0) + internal GridChildrenCollectionEnumeratorSimple(Grid grid, bool includeChildren) { - throw new ArgumentException("Invalid Grid.Row value."); + Debug.Assert(grid != null); + _currentEnumerator = -1; + _enumerator0 = new ColumnDefinitions.Enumerator(grid.ExtData != null ? grid.ExtData.ColumnDefinitions : null); + _enumerator1 = new RowDefinitions.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; + } } - return value; - } + 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) + { + throw new InvalidOperationException(SR.Get(SRID.EnumeratorNotStarted)); + } + if (_currentEnumerator >= 3) + { + throw new InvalidOperationException(SR.Get(SRID.EnumeratorReachedEnd)); + } + + // assert below is not true anymore since Controls 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 ColumnDefinitions.Enumerator _enumerator0; + private RowDefinitions.Enumerator _enumerator1; + private Controls _enumerator2Collection; + private int _enumerator2Index; + private int _enumerator2Count; + }*/ /// - /// Called when the value of changes for a control. + /// Helper to render grid lines. /// - /// The control that triggered the change. - /// Change arguments. - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + internal class GridLinesRenderer : Control { - var shouldDispose = (arg2.OldValue is bool d) && d; - if (shouldDispose) + /// + /// Static initialization + /// + static GridLinesRenderer() { - var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; - if (host == null) - throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); - host.Dispose(); - source.ClearValue(s_sharedSizeScopeHostProperty); + var dashArray = new List() { _dashLength, _dashLength }; + + var ds1 = new DashStyle(dashArray, 0); + _oddDashPen = new Pen(Brushes.Blue, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); + + var ds2 = new DashStyle(dashArray, _dashLength); + _evenDashPen = new Pen(Brushes.Yellow, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); } - var shouldAssign = (arg2.NewValue is bool a) && a; - if (shouldAssign) + /// + /// UpdateRenderBounds. + /// + public override void Render(DrawingContext drawingContext) { - if (source.GetValue(s_sharedSizeScopeHostProperty) != null) - throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); - source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost()); + var grid = this.GetVisualParent(); + + if (grid == null || !grid.ShowGridLines) + return; + + for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].FinalOffset, 0.0, + grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height); + } + + for (int i = 1; i < grid.RowDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].FinalOffset, + _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); + } } - // if the scope has changed, notify the descendant grids that they need to update. - if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) { - var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); - - foreach (var grid in participatingGrids) - grid.SharedScopeChanged(); + var start = new Point(startX, startY); + var end = new Point(endX, endY); + drawingContext.DrawLine(_oddDashPen, start, end); + drawingContext.DrawLine(_evenDashPen, start, end); + } + internal void UpdateRenderBounds(Size arrangeSize) + { + _lastArrangeSize = arrangeSize; + this.InvalidateMeasure(); + this.InvalidateVisual(); } - } + + private static Size _lastArrangeSize; + private const double _dashLength = 4.0; // + private const double _penWidth = 1.0; // + private static readonly Pen _oddDashPen; // first pen to draw dash + private static readonly Pen _evenDashPen; // second pen to draw dash + } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d0ab0a0c8b..5f01c233b8 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -339,7 +339,7 @@ namespace Avalonia.Controls var point = e.GetPointerPoint(null); RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, - point.Properties, e.InputModifiers)); + e.Timestamp, point.Properties, e.InputModifiers)); } /// @@ -349,7 +349,7 @@ namespace Avalonia.Controls var point = e.GetPointerPoint(null); RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, - point.Properties, e.InputModifiers)); + e.Timestamp, point.Properties, e.InputModifiers)); } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 30330ef9ac..e7d8018a42 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; @@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters private Vector _offset; private IDisposable _logicalScrollSubscription; private Size _viewport; + private Dictionary _activeLogicalGestureScrolls; /// /// Initializes static members of the class. @@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters public ScrollContentPresenter() { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); + AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } @@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters return finalSize; } + // Arbitrary chosen value, probably need to ask ILogicalScrollable + private const int LogicalScrollItemSize = 50; + private void OnScrollGesture(object sender, ScrollGestureEventArgs e) + { + if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) + { + var scrollable = Child as ILogicalScrollable; + bool isLogical = scrollable?.IsLogicalScrollEnabled == true; + + double x = Offset.X; + double y = Offset.Y; + + Vector delta = default; + if (isLogical) + _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); + delta += e.Delta; + + if (Extent.Height > Viewport.Height) + { + double dy; + if (isLogical) + { + var logicalUnits = delta.Y / LogicalScrollItemSize; + delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); + dy = logicalUnits * scrollable.ScrollSize.Height; + } + else + dy = delta.Y; + + + y += dy; + y = Math.Max(y, 0); + y = Math.Min(y, Extent.Height - Viewport.Height); + } + + if (Extent.Width > Viewport.Width) + { + double dx; + if (isLogical) + { + var logicalUnits = delta.X / LogicalScrollItemSize; + delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); + dx = logicalUnits * scrollable.ScrollSize.Width; + } + else + dx = delta.X; + x += dx; + x = Math.Max(x, 0); + x = Math.Min(x, Extent.Width - Viewport.Width); + } + + if (isLogical) + { + if (_activeLogicalGestureScrolls == null) + _activeLogicalGestureScrolls = new Dictionary(); + _activeLogicalGestureScrolls[e.Id] = delta; + } + + Offset = new Vector(x, y); + e.Handled = true; + } + } + + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + => _activeLogicalGestureScrolls?.Remove(e.Id); + /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 7307843417..ad7312d515 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public RowDefinition() + public RowDefinition() { } @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// The height of the row. /// The height unit of the column. - public RowDefinition(double value, GridUnitType type) + public RowDefinition(double value, GridUnitType type) { Height = new GridLength(value, type); } @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The height of the column. - public RowDefinition(GridLength height) + public RowDefinition(GridLength height) { Height = height; } @@ -55,11 +55,7 @@ namespace Avalonia.Controls /// /// Gets the actual calculated height of the row. /// - public double ActualHeight - { - get; - internal set; - } + public double ActualHeight => Parent?.GetFinalRowDefinitionHeight(Index) ?? 0d; /// /// Gets or sets the maximum height of the row in DIPs. @@ -87,5 +83,9 @@ namespace Avalonia.Controls get { return GetValue(HeightProperty); } set { SetValue(HeightProperty, value); } } + + internal override GridLength UserSizeValueCache => this.Height; + internal override double UserMinSizeValueCache => this.MinHeight; + internal override double UserMaxSizeValueCache => this.MaxHeight; } } \ No newline at end of file diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/RowDefinitions.cs index 1a14cc78f3..3090844251 100644 --- a/src/Avalonia.Controls/RowDefinitions.cs +++ b/src/Avalonia.Controls/RowDefinitions.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls /// /// A collection of s. /// - public class RowDefinitions : AvaloniaList + public class RowDefinitions : DefinitionList { /// /// Initializes a new instance of the class. @@ -17,6 +17,7 @@ namespace Avalonia.Controls public RowDefinitions() { ResetBehavior = ResetBehavior.Remove; + CollectionChanged += OnCollectionChanged; } /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index d6537ebbca..fc2c118132 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; @@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -166,10 +168,24 @@ namespace Avalonia.Controls { base.OnPointerPressed(e); - if (e.MouseButton == MouseButton.Left) + if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse) { e.Handled = UpdateSelectionFromEventSource(e.Source); } } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse) + { + var container = GetContainerFromEventSource(e.Source); + if (container != null + && container.GetVisualsAt(e.GetPosition(container)) + .Any(c => container == c || container.IsVisualAncestorOf(c))) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } + } + } } } diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs deleted file mode 100644 index 7704228a4e..0000000000 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ /dev/null @@ -1,705 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Layout; -using JetBrains.Annotations; - -namespace Avalonia.Controls.Utils -{ - /// - /// Contains algorithms that can help to measure and arrange a Grid. - /// - internal class GridLayout - { - /// - /// Initialize a new instance from the column definitions. - /// The instance doesn't care about whether the definitions are rows or columns. - /// It will not calculate the column or row differently. - /// - internal GridLayout([NotNull] ColumnDefinitions columns) - { - if (columns == null) throw new ArgumentNullException(nameof(columns)); - _conventions = columns.Count == 0 - ? new List { new LengthConvention() } - : columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); - } - - /// - /// Initialize a new instance from the row definitions. - /// The instance doesn't care about whether the definitions are rows or columns. - /// It will not calculate the column or row differently. - /// - internal GridLayout([NotNull] RowDefinitions rows) - { - if (rows == null) throw new ArgumentNullException(nameof(rows)); - _conventions = rows.Count == 0 - ? new List { new LengthConvention() } - : rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); - } - - /// - /// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same. - /// - private const double LayoutTolerance = 1.0 / 256.0; - - /// - /// Gets all the length conventions that come from column/row definitions. - /// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count. - /// - [NotNull] - private readonly List _conventions; - - /// - /// Gets all the length conventions that come from the grid children. - /// - [NotNull] - private readonly List _additionalConventions = - new List(); - - /// - /// Appending these elements into the convention list helps lay them out according to their desired sizes. - /// - /// Some elements are not only in a single grid cell, they have one or more column/row spans, - /// and these elements may affect the grid layout especially the measuring procedure. - /// Append these elements into the convention list can help to layout them correctly through - /// their desired size. Only a small subset of children need to be measured before layout starts - /// and they will be called via the callback. - /// - /// The grid children type. - /// - /// Contains the safe column/row index and its span. - /// Notice that we will not verify whether the range is in the column/row count, - /// so you should get the safe column/row info first. - /// - /// - /// This callback will be called if the thinks that a child should be - /// measured first. Usually, these are the children that have the * or Auto length. - /// - internal void AppendMeasureConventions([NotNull] IDictionary source, - [NotNull] Func getDesiredLength) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); - - // M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.) - // Only these columns/rows' layout can be affected by the child desired size. - // - // Find all columns/rows that have Auto or * length. We'll measure the children in advance. - // Only these kind of columns/rows will affect the Grid layout. - // Please note: - // - If the column / row has Auto length, the Grid.DesiredSize and the column width - // will be affected by the child's desired size. - // - If the column / row has* length, the Grid.DesiredSize will be affected by the - // child's desired size but the column width not. - - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // _conventions: | min | max | | | min | | min max | max | - // _additionalC: |<- desired ->| |< desired >| - // _additionalC: |< desired >| |<- desired ->| - - // 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。 - // 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。 - // 请注意: - // - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize; - // - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。 - - // Find all the Auto and * length columns/rows. - var found = new Dictionary(); - for (var i = 0; i < _conventions.Count; i++) - { - var index = i; - var convention = _conventions[index]; - if (convention.Length.IsAuto || convention.Length.IsStar) - { - foreach (var pair in source.Where(x => - x.Value.index <= index && index < x.Value.index + x.Value.span)) - { - found[pair.Key] = pair.Value; - } - } - } - - // Append these layout into the additional convention list. - foreach (var pair in found) - { - var t = pair.Key; - var (index, span) = pair.Value; - var desiredLength = getDesiredLength(t); - if (Math.Abs(desiredLength) > LayoutTolerance) - { - _additionalConventions.Add(new AdditionalLengthConvention(index, span, desiredLength)); - } - } - } - - /// - /// Run measure procedure according to the and gets the . - /// - /// - /// The container length. Usually, it is the constraint of the method. - /// - /// - /// Overriding conventions that allows the algorithm to handle external inputa - /// - /// - /// The measured result that containing the desired size and all the column/row lengths. - /// - [NotNull, Pure] - internal MeasureResult Measure(double containerLength, IReadOnlyList conventions = null) - { - // Prepare all the variables that this method needs to use. - conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); - var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - var aggregatedLength = 0.0; - double starUnitLength; - - // M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`. - // We mark the aggregated length as "fix" because we can completely determine their values. Same as below. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // |#fix#| |#fix#| - // - // 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。 - // 我们会将所有能够确定下长度的行列标记为 fix。下同。 - // 请注意: - // - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。 - - aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // M3/7. Fix all the * lengths that have reached the minimum. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // | fix | |#fix#| fix | - - var shouldTestStarMin = true; - while (shouldTestStarMin) - { - // Calculate the unit * length to estimate the length of each column/row that has * length. - // Under this estimated length, check if there is a minimum value that has a length less than its constraint. - // If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value. - // - // 计算单位 * 的长度,以便预估出每一个 * 行列的长度。 - // 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。 - // 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。 - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - foreach (var convention in conventions.Where(x => x.Length.IsStar)) - { - var (star, min) = (convention.Length.Value, convention.MinLength); - var starLength = star * starUnitLength; - if (starLength < min) - { - convention.Fix(min); - starLength = min; - aggregatedLength += starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMin = @fixed; - } - - // M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| | fix |#fix#| fix | fix | - - var shouldTestAuto = true; - while (shouldTestAuto) - { - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - for (var i = 0; i < conventions.Count; i++) - { - var convention = conventions[i]; - if (!convention.Length.IsAuto) - { - continue; - } - - var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); - convention.Fix(more); - aggregatedLength += more; - @fixed = true; - break; - } - - shouldTestAuto = @fixed; - } - - // M5/7. Expand the stars according to the additional conventions (usually the child desired length). - // We can't fix this kind of length, so we just mark them as desired (des). - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - - var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); - aggregatedLength += desiredStarMin; - - // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. - // Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - // Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure. - // - // desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des) - // greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des - - var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; - var greedyDesiredLength = aggregatedLength; - - // M7/7. Expand all the rest stars. These stars have no conventions or only have - // max value they can be expanded from zero to constraint. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#| - // Note: This table will be stored as the final result into the MeasureResult. - - var dynamicConvention = ExpandStars(conventions, containerLength); - Clip(dynamicConvention, containerLength); - - // Returns the measuring result. - return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, - conventions, dynamicConvention, minLengths); - } - - /// - /// Run arrange procedure according to the and gets the . - /// - /// - /// The container length. Usually, it is the finalSize of the method. - /// - /// - /// The result that the measuring procedure returns. If it is null, a new measure procedure will run. - /// - /// - /// The measured result that containing the desired size and all the column/row length. - /// - [NotNull, Pure] - public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) - { - measure = measure ?? Measure(finalLength); - - // If the arrange final length does not equal to the measure length, we should measure again. - if (finalLength - measure.ContainerLength > LayoutTolerance) - { - // If the final length is larger, we will rerun the whole measure. - measure = Measure(finalLength, measure.LeanLengthList); - } - else if (finalLength - measure.ContainerLength < -LayoutTolerance) - { - // If the final length is smaller, we measure the M6/6 procedure only. - var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); - measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, - measure.LeanLengthList, dynamicConvention, measure.MinLengths); - } - - return new ArrangeResult(measure.LengthList); - } - - /// - /// Use the to calculate the fixed length of the Auto column/row. - /// - /// The convention list that all the * with minimum length are fixed. - /// The column/row index that should be fixed. - /// The unit * length for the current rest length. - /// The final length of the Auto length column/row. - [Pure] - private double ApplyAdditionalConventionsForAuto(IReadOnlyList conventions, - int index, double starUnitLength) - { - // 1. Calculate all the * length with starUnitLength. - // 2. Exclude all the fixed length and all the * length. - // 3. Compare the rest of the desired length and the convention. - // +-----------------+ - // | * | A | * | - // +-----------------+ - // | exl | | exl | - // |< desired >| - // |< desired >| - - var more = 0.0; - foreach (var additional in _additionalConventions) - { - // If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length. - if (index == additional.Index + additional.Span - 1) - { - var min = Enumerable.Range(additional.Index, additional.Span) - .Select(x => - { - var c = conventions[x]; - if (c.Length.IsAbsolute) return c.Length.Value; - if (c.Length.IsStar) return c.Length.Value * starUnitLength; - return 0.0; - }).Sum(); - more = Math.Max(additional.Min - min, more); - } - } - - return Math.Min(conventions[index].MaxLength, more); - } - - /// - /// Calculate the total desired length of all the * length. - /// Bug Warning: - /// - The behavior of this method is undefined! Different UI Frameworks have different behaviors. - /// - We ignore all the span columns/rows and just take single cells into consideration. - /// - /// All the conventions that have almost been fixed except the rest *. - /// The total desired length of all the * length. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private (List, double) AggregateAdditionalConventionsForStars( - IReadOnlyList conventions) - { - // 1. Determine all one-span column's desired widths or row's desired heights. - // 2. Order the multi-span conventions by its last index - // (Notice that the sorted data is much smaller than the source.) - // 3. Determine each multi-span last index by calculating the maximum desired size. - - // Before we determine the behavior of this method, we just aggregate the one-span * columns. - - var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // Prepare a lengthList variable indicating the fixed length of each column/row. - var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); - foreach (var group in _additionalConventions - .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) - .ToLookup(x => x.Index)) - { - lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); - } - - // Now the lengthList is fixed by every one-span columns/rows. - // Then we should determine the multi-span column's/row's length. - foreach (var group in _additionalConventions - .Where(x => x.Span > 1) - .ToLookup(x => x.Index + x.Span - 1) - // Order the multi-span columns/rows by last index. - .OrderBy(x => x.Key)) - { - var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); - lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); - } - - return (lengthList, lengthList.Sum() - fixedLength); - } - - /// - /// This method implements the last procedure (M7/7) of measure. - /// It expands all the * length to the fixed length according to the . - /// - /// All the conventions that have almost been fixed except the remaining *. - /// The container length. - /// The final pixel length list. - [Pure] - private static List ExpandStars(IEnumerable conventions, double constraint) - { - // Initial. - var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); - constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - var starUnitLength = 0.0; - - // M6/6. - if (constraint >= 0) - { - var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - - var shouldTestStarMax = true; - while (shouldTestStarMax) - { - var @fixed = false; - starUnitLength = constraint / starCount; - foreach (var convention in dynamicConvention.Where(x => - x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) - { - var (star, max) = (convention.Length.Value, convention.MaxLength); - var starLength = star * starUnitLength; - if (starLength > max) - { - convention.Fix(max); - starLength = max; - constraint -= starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMax = @fixed; - } - } - - Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto)); - - var starUnit = starUnitLength; - var result = dynamicConvention.Select(x => - { - if (x.Length.IsStar) - { - return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value; - } - - return x.Length.Value; - }).ToList(); - - return result; - } - - /// - /// If the container length is not infinity. It may be not enough to contain all the columns/rows. - /// We should clip the columns/rows that have been out of the container bounds. - /// Note: This method may change the items value of . - /// - /// A list of all the column widths and row heights with a fixed pixel length - /// the container length. It can be positive infinity. - private static void Clip([NotNull] IList lengthList, double constraint) - { - if (double.IsInfinity(constraint)) - { - return; - } - - var measureLength = 0.0; - for (var i = 0; i < lengthList.Count; i++) - { - var length = lengthList[i]; - if (constraint - measureLength > length) - { - measureLength += length; - } - else - { - lengthList[i] = constraint - measureLength; - measureLength = constraint; - } - } - } - - /// - /// Contains the convention of each column/row. - /// This is mostly the same as or . - /// We use this because we can treat the column and the row the same. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal class LengthConvention : ICloneable - { - /// - /// Initialize a new instance of . - /// - public LengthConvention() - { - Length = new GridLength(1.0, GridUnitType.Star); - MinLength = 0.0; - MaxLength = double.PositiveInfinity; - } - - /// - /// Initialize a new instance of . - /// - public LengthConvention(GridLength length, double minLength, double maxLength) - { - Length = length; - MinLength = minLength; - MaxLength = maxLength; - if (length.IsAbsolute) - { - _isFixed = true; - } - } - - /// - /// Gets the of a column or a row. - /// - internal GridLength Length { get; private set; } - - /// - /// Gets the minimum convention for a column or a row. - /// - internal double MinLength { get; } - - /// - /// Gets the maximum convention for a column or a row. - /// - internal double MaxLength { get; } - - /// - /// Fix the . - /// If all columns/rows are fixed, we can get the size of all columns/rows in pixels. - /// - /// - /// The pixel length that should be used to fix the convention. - /// - /// - /// If the convention is pixel length, this exception will throw. - /// - public void Fix(double pixel) - { - if (_isFixed) - { - throw new InvalidOperationException("Cannot fix the length convention if it is fixed."); - } - - Length = new GridLength(pixel); - _isFixed = true; - } - - /// - /// Gets a value that indicates whether this convention is fixed. - /// - private bool _isFixed; - - /// - /// Helps the debugger to display the intermediate column/row calculation result. - /// - private string DebuggerDisplay => - $"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; - - /// - object ICloneable.Clone() => Clone(); - - /// - /// Get a deep copy of this convention list. - /// We need this because we want to store some intermediate states. - /// - internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); - } - - /// - /// Contains the convention that comes from the grid children. - /// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal struct AdditionalLengthConvention - { - /// - /// Initialize a new instance of . - /// - public AdditionalLengthConvention(int index, int span, double min) - { - Index = index; - Span = span; - Min = min; - } - - /// - /// Gets the start index of this additional convention. - /// - public int Index { get; } - - /// - /// Gets the span of this additional convention. - /// - public int Span { get; } - - /// - /// Gets the minimum length of this additional convention. - /// This value is usually provided by the child's desired length. - /// - public double Min { get; } - - /// - /// Helps the debugger to display the intermediate column/row calculation result. - /// - private string DebuggerDisplay => - $"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; - } - - /// - /// Stores the result of the measuring procedure. - /// This result can be used to measure children and assign the desired size. - /// Passing this result to can reduce calculation. - /// - [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] - internal class MeasureResult - { - /// - /// Initialize a new instance of . - /// - internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, - IReadOnlyList leanConventions, IReadOnlyList expandedConventions, IReadOnlyList minLengths) - { - ContainerLength = containerLength; - DesiredLength = desiredLength; - GreedyDesiredLength = greedyDesiredLength; - LeanLengthList = leanConventions; - LengthList = expandedConventions; - MinLengths = minLengths; - } - - /// - /// Gets the container length for this result. - /// This property will be used by to determine whether to measure again or not. - /// - public double ContainerLength { get; } - - /// - /// Gets the desired length of this result. - /// Just return this value as the desired size in . - /// - public double DesiredLength { get; } - - /// - /// Gets the desired length if the container has infinite length. - /// - public double GreedyDesiredLength { get; } - - /// - /// Contains the column/row calculation intermediate result. - /// This value is used by for reducing repeat calculation. - /// - public IReadOnlyList LeanLengthList { get; } - - /// - /// Gets the length list for each column/row. - /// - public IReadOnlyList LengthList { get; } - public IReadOnlyList MinLengths { get; } - } - - /// - /// Stores the result of the measuring procedure. - /// This result can be used to arrange children and assign the render size. - /// - [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] - internal class ArrangeResult - { - /// - /// Initialize a new instance of . - /// - internal ArrangeResult(IReadOnlyList lengthList) - { - LengthList = lengthList; - } - - /// - /// Gets the length list for each column/row. - /// - public IReadOnlyList LengthList { get; } - } - } -} diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs deleted file mode 100644 index 8553165e4b..0000000000 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ /dev/null @@ -1,651 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.Layout; -using Avalonia.VisualTree; - -namespace Avalonia.Controls -{ - /// - /// Shared size scope implementation. - /// Shares the size information between participating grids. - /// An instance of this class is attached to every that has its - /// IsSharedSizeScope property set to true. - /// - internal sealed class SharedSizeScopeHost : IDisposable - { - private enum MeasurementState - { - Invalidated, - Measuring, - Cached - } - - /// - /// Class containing the measured rows/columns for a single grid. - /// Monitors changes to the row/column collections as well as the SharedSizeGroup changes - /// for the individual items in those collections. - /// Notifies the of SharedSizeGroup changes. - /// - private sealed class MeasurementCache : IDisposable - { - readonly CompositeDisposable _subscriptions; - readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); - - public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; - - public MeasurementCache(Grid grid) - { - Grid = grid; - Results = grid.RowDefinitions.Cast() - .Concat(grid.ColumnDefinitions) - .Select(d => new MeasurementResult(grid, d)) - .ToList(); - - grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; - grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; - - - _subscriptions = new CompositeDisposable( - Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), - grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); - - } - - // method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid - private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - // route to collection changed as a Reset. - DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - private void DefinitionPropertyChanged(Tuple propertyChanged) - { - if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) - { - var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); - var oldName = result.SizeGroup?.Name; - var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; - _groupChanged.OnNext((oldName, newName, result)); - } - } - - private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - int offset = 0; - if (sender is ColumnDefinitions) - offset = Grid.RowDefinitions.Count; - - var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List(); - var oldItems = e.OldStartingIndex >= 0 - ? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) - : new List(); - - void NotifyNewItems() - { - foreach (var item in newItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); - } - } - - void NotifyOldItems() - { - foreach (var item in oldItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); - } - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Results.InsertRange(e.NewStartingIndex + offset, newItems); - NotifyNewItems(); - break; - - case NotifyCollectionChangedAction.Remove: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - NotifyOldItems(); - break; - - case NotifyCollectionChangedAction.Move: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, oldItems); - break; - - case NotifyCollectionChangedAction.Replace: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, newItems); - - NotifyOldItems(); - NotifyNewItems(); - - break; - - case NotifyCollectionChangedAction.Reset: - oldItems = Results; - newItems = Results = Grid.RowDefinitions.Cast() - .Concat(Grid.ColumnDefinitions) - .Select(d => new MeasurementResult(Grid, d)) - .ToList(); - NotifyOldItems(); - NotifyNewItems(); - - break; - } - } - - - /// - /// Updates the Results collection with Grid Measure results. - /// - /// Result of the GridLayout.Measure method for the RowDefinitions in the grid. - /// Result of the GridLayout.Measure method for the ColumnDefinitions in the grid. - public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - MeasurementState = MeasurementState.Cached; - for (int i = 0; i < Grid.RowDefinitions.Count; i++) - { - Results[i].MeasuredResult = rowResult.LengthList[i]; - Results[i].MinLength = rowResult.MinLengths[i]; - } - - for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) - { - Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; - Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; - } - } - - /// - /// Clears the measurement cache, in preparation for the Measure pass. - /// - public void InvalidateMeasure() - { - var newItems = new List(); - var oldItems = new List(); - - MeasurementState = MeasurementState.Invalidated; - - Results.ForEach(r => - { - r.MeasuredResult = double.NaN; - r.SizeGroup?.Reset(); - }); - } - - /// - /// Clears the subscriptions. - /// - public void Dispose() - { - _subscriptions.Dispose(); - _groupChanged.OnCompleted(); - } - - /// - /// Gets the for which this cache has been created. - /// - public Grid Grid { get; } - - /// - /// Gets the of this cache. - /// - public MeasurementState MeasurementState { get; private set; } - - /// - /// Gets the list of instances. - /// - /// - /// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions - /// - public List Results { get; private set; } - } - - - /// - /// Class containing the Measure result for a single Row/Column in a grid. - /// - private class MeasurementResult - { - public MeasurementResult(Grid owningGrid, DefinitionBase definition) - { - OwningGrid = owningGrid; - Definition = definition; - MeasuredResult = double.NaN; - } - - /// - /// Gets the / related to this - /// - public DefinitionBase Definition { get; } - - /// - /// Gets or sets the actual result of the Measure operation for this column. - /// - public double MeasuredResult { get; set; } - - /// - /// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids. - /// - public double MinLength { get; set; } - - /// - /// Gets or sets the that this result belongs to. - /// - public Group SizeGroup { get; set; } - - /// - /// Gets the Grid that is the parent of the Row/Column - /// - public Grid OwningGrid { get; } - - /// - /// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup. - /// - /// A tuple of length and the priority in the shared size group. - public (double length, int priority) GetPriorityLength() - { - var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; - - if (length.IsAbsolute) - return (MeasuredResult, 1); - if (length.IsAuto) - return (MeasuredResult, 2); - if (MinLength > 0) - return (MinLength, 3); - return (MeasuredResult, 4); - } - } - - /// - /// Visitor class used to gather the final length for a given SharedSizeGroup. - /// - /// - /// The values are applied according to priorities defined in . - /// - private class LentgthGatherer - { - /// - /// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup - /// - public double Length { get; private set; } - private int gatheredPriority = 6; - - /// - /// Visits the applying the result of to its internal cache. - /// - /// The instance to visit. - public void Visit(MeasurementResult result) - { - var (length, priority) = result.GetPriorityLength(); - - if (gatheredPriority < priority) - return; - - gatheredPriority = priority; - if (gatheredPriority == priority) - { - Length = Math.Max(length,Length); - } - else - { - Length = length; - } - } - } - - /// - /// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value. - /// - private class Group - { - private double? cachedResult; - private List _results = new List(); - - /// - /// Gets the name of the SharedSizeGroup. - /// - public string Name { get; } - - public Group(string name) - { - Name = name; - } - - /// - /// Gets the collection of the instances. - /// - public IReadOnlyList Results => _results; - - /// - /// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup. - /// - public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; - - /// - /// Clears the previously cached result in preparation for measurement. - /// - public void Reset() - { - cachedResult = null; - } - - /// - /// Ads a measurement result to this group and sets it's property - /// to this instance. - /// - /// The to include in this group. - public void Add(MeasurementResult result) - { - if (_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); - - result.SizeGroup = this; - _results.Add(result); - } - - /// - /// Removes the measurement result from this group and clears its value. - /// - /// The to clear. - public void Remove(MeasurementResult result) - { - if (!_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); - result.SizeGroup = null; - _results.Remove(result); - } - - - private double Gather() - { - var visitor = new LentgthGatherer(); - - _results.ForEach(visitor.Visit); - - return visitor.Length; - } - } - - private readonly AvaloniaList _measurementCaches = new AvaloniaList(); - private readonly Dictionary _groups = new Dictionary(); - private bool _invalidating; - - /// - /// Removes the SharedSizeScope and notifies all affected grids of the change. - /// - public void Dispose() - { - while (_measurementCaches.Any()) - _measurementCaches[0].Grid.SharedScopeChanged(); - } - - /// - /// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree. - /// - /// The to add to this scope. - internal void RegisterGrid(Grid toAdd) - { - if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); - - var cache = new MeasurementCache(toAdd); - _measurementCaches.Add(cache); - AddGridToScopes(cache); - } - - /// - /// Removes the registration for a grid in this SharedSizeScope. - /// - /// The to remove. - internal void UnegisterGrid(Grid toRemove) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); - - _measurementCaches.Remove(cache); - RemoveGridFromScopes(cache); - cache.Dispose(); - } - - /// - /// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope. - /// - /// The that should be checked. - /// True if the grid should forward its calls. - internal bool ParticipatesInScope(Grid toCheck) - { - return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) - ?.Results.Any(r => r.SizeGroup != null) ?? false; - } - - /// - /// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated. - /// Forwards the same call to all affected grids in this scope. - /// - /// The that had it's Measure invalidated. - internal void InvalidateMeasure(Grid grid) - { - // prevent stack overflow - if (_invalidating) - return; - _invalidating = true; - - InvalidateMeasureImpl(grid); - - _invalidating = false; - } - - /// - /// Updates the measurement cache with the results of the measurement pass. - /// - /// The that has been measured. - /// Measurement result for the grid's - /// Measurement result for the grid's - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); - - cache.UpdateMeasureResult(rowResult, columnResult); - } - - /// - /// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid. - /// - /// The that is being Arranged - /// The 's cached measurement result. - /// The 's cached measurement result. - /// Row and column measurement result updated with the SharedSizeScope constraints. - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - return ( - Arrange(grid.RowDefinitions, rowResult), - Arrange(grid.ColumnDefinitions, columnResult) - ); - } - - /// - /// Invalidates the measure of all grids affected by the SharedSizeGroups contained within. - /// - /// The that is being invalidated. - private void InvalidateMeasureImpl(Grid grid) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - - if (cache == null) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); - - // already invalidated the cache, early out. - if (cache.MeasurementState == MeasurementState.Invalidated) - return; - - // we won't calculate, so we should not invalidate. - if (!ParticipatesInScope(grid)) - return; - - cache.InvalidateMeasure(); - - // maybe there is a condition to only call arrange on some of the calls? - grid.InvalidateMeasure(); - - // find all the scopes within the invalidated grid - var scopeNames = cache.Results - .Where(mr => mr.SizeGroup != null) - .Select(mr => mr.SizeGroup.Name) - .Distinct(); - // find all grids related to those scopes - var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) - .Select(r => r.OwningGrid) - .Where(g => g.IsMeasureValid) - .Distinct(); - - // invalidate them as well - foreach (var otherGrid in otherGrids) - { - InvalidateMeasureImpl(otherGrid); - } - } - - /// - /// callback notifying the scope that a has changed its - /// SharedSizeGroup - /// - /// Old and New name (either can be null) of the SharedSizeGroup, as well as the result. - private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) - { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); - } - - /// - /// Handles the impact of SharedSizeGroups on the Arrange of / - /// - /// Rows/Columns that were measured - /// The initial measurement result. - /// Modified measure result - private GridLayout.MeasureResult Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) - { - var conventions = measureResult.LeanLengthList.ToList(); - var lengths = measureResult.LengthList.ToList(); - var desiredLength = 0.0; - for (int i = 0; i < definitions.Count; i++) - { - var definition = definitions[i]; - - // for empty SharedSizeGroups pass on unmodified result. - if (string.IsNullOrEmpty(definition.SharedSizeGroup)) - { - desiredLength += measureResult.LengthList[i]; - continue; - } - - var group = _groups[definition.SharedSizeGroup]; - // Length calculated over all Definitions participating in a SharedSizeGroup. - var length = group.CalculatedLength; - - conventions[i] = new GridLayout.LengthConvention( - new GridLength(length), - measureResult.LeanLengthList[i].MinLength, - measureResult.LeanLengthList[i].MaxLength - ); - lengths[i] = length; - desiredLength += length; - } - - return new GridLayout.MeasureResult( - measureResult.ContainerLength, - desiredLength, - measureResult.GreedyDesiredLength,//?? - conventions, - lengths, - measureResult.MinLengths); - } - - /// - /// Adds all measurement results for a grid to their repsective scopes. - /// - /// The for a grid to be added. - private void AddGridToScopes(MeasurementCache cache) - { - cache.GroupChanged.Subscribe(SharedGroupChanged); - - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - AddToGroup(scopeName, result); - } - } - - /// - /// Handles adding the to a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to add the to. - /// The to add to a scope. - private void AddToGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group(scopeName)); - - group.Add(result); - } - - /// - /// Removes all measurement results for a grid from their respective scopes. - /// - /// The for a grid to be removed. - private void RemoveGridFromScopes(MeasurementCache cache) - { - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - RemoveFromGroup(scopeName, result); - } - } - - /// - /// Handles removing the from a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to remove the from. - /// The to remove from a scope. - private void RemoveFromGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); - - group.Remove(result); - if (!group.Results.Any()) - _groups.Remove(scopeName); - } - } -} diff --git a/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs new file mode 100644 index 0000000000..91b224e65a --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.LogicalTree; +using Avalonia.Styling; + +namespace Avalonia.Input.GestureRecognizers +{ + public class GestureRecognizerCollection : IReadOnlyCollection, IGestureRecognizerActionsDispatcher + { + private readonly IInputElement _inputElement; + private List _recognizers; + private Dictionary _pointerGrabs; + + + public GestureRecognizerCollection(IInputElement inputElement) + { + _inputElement = inputElement; + } + + public void Add(IGestureRecognizer recognizer) + { + if (_recognizers == null) + { + // We initialize the collection when the first recognizer is added + _recognizers = new List(); + _pointerGrabs = new Dictionary(); + } + + _recognizers.Add(recognizer); + recognizer.Initialize(_inputElement, this); + + // Hacks to make bindings work + + if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical) + { + logical.SetParent(logicalParent); + if (recognizer is IStyleable styleableRecognizer + && _inputElement is IStyleable styleableParent) + styleableRecognizer.Bind(StyledElement.TemplatedParentProperty, + styleableParent.GetObservable(StyledElement.TemplatedParentProperty)); + } + } + + static readonly List s_Empty = new List(); + + public IEnumerator GetEnumerator() + => _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _recognizers?.Count ?? 0; + + + internal bool HandlePointerPressed(PointerPressedEventArgs e) + { + if (_recognizers == null) + return false; + foreach (var r in _recognizers) + { + if(e.Handled) + break; + r.PointerPressed(e); + } + + return e.Handled; + } + + internal bool HandlePointerReleased(PointerReleasedEventArgs e) + { + if (_recognizers == null) + return false; + if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + { + capture.PointerReleased(e); + } + else + foreach (var r in _recognizers) + { + if (e.Handled) + break; + r.PointerReleased(e); + } + return e.Handled; + } + + internal bool HandlePointerMoved(PointerEventArgs e) + { + if (_recognizers == null) + return false; + if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) + { + capture.PointerMoved(e); + } + else + foreach (var r in _recognizers) + { + if (e.Handled) + break; + r.PointerMoved(e); + } + return e.Handled; + } + + internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (_recognizers == null) + return; + _pointerGrabs.Remove(e.Pointer); + foreach (var r in _recognizers) + { + if(e.Handled) + break; + r.PointerCaptureLost(e); + } + } + + void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) + { + pointer.Capture(_inputElement); + _pointerGrabs[pointer] = recognizer; + } + + } +} diff --git a/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs new file mode 100644 index 0000000000..b8ba9e529c --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Input.GestureRecognizers +{ + public interface IGestureRecognizer + { + void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions); + void PointerPressed(PointerPressedEventArgs e); + void PointerReleased(PointerReleasedEventArgs e); + void PointerMoved(PointerEventArgs e); + void PointerCaptureLost(PointerCaptureLostEventArgs e); + } + + public interface IGestureRecognizerActionsDispatcher + { + void Capture(IPointer pointer, IGestureRecognizer recognizer); + } + + public enum GestureRecognizerResult + { + None, + Capture, + ReleaseCapture + } +} diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs new file mode 100644 index 0000000000..4f3c7c0bba --- /dev/null +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -0,0 +1,183 @@ +using System; +using System.Diagnostics; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace Avalonia.Input.GestureRecognizers +{ + public class ScrollGestureRecognizer + : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise + IGestureRecognizer + { + private bool _scrolling; + private Point _trackedRootPoint; + private IPointer _tracking; + private IInputElement _target; + private IGestureRecognizerActionsDispatcher _actions; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; + private int _gestureId; + + // Movement per second + private Vector _inertia; + private ulong? _lastMoveTimestamp; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll, + (o, v) => o.CanHorizontallyScroll = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll, + (o, v) => o.CanVerticallyScroll = v); + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanHorizontallyScroll + { + get => _canHorizontallyScroll; + set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); + } + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanVerticallyScroll + { + get => _canVerticallyScroll; + set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); + } + + + public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) + { + _target = target; + _actions = actions; + } + + public void PointerPressed(PointerPressedEventArgs e) + { + if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch) + { + EndGesture(); + _tracking = e.Pointer; + _gestureId = ScrollGestureEventArgs.GetNextFreeId();; + _trackedRootPoint = e.GetPosition(null); + } + } + + // Arbitrary chosen value, probably need to move that to platform settings or something + private const double ScrollStartDistance = 30; + + // Pixels per second speed that is considered to be the stop of inertiall scroll + private const double InertialScrollSpeedEnd = 5; + + public void PointerMoved(PointerEventArgs e) + { + if (e.Pointer == _tracking) + { + var rootPoint = e.GetPosition(null); + if (!_scrolling) + { + if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance) + _scrolling = true; + if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance) + _scrolling = true; + if (_scrolling) + { + _actions.Capture(e.Pointer, this); + } + } + + if (_scrolling) + { + var vector = _trackedRootPoint - rootPoint; + var elapsed = _lastMoveTimestamp.HasValue ? + TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : + TimeSpan.Zero; + + _lastMoveTimestamp = e.Timestamp; + _trackedRootPoint = rootPoint; + if (elapsed.TotalSeconds > 0) + _inertia = vector / elapsed.TotalSeconds; + _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); + e.Handled = true; + } + } + } + + public void PointerCaptureLost(PointerCaptureLostEventArgs e) + { + if (e.Pointer == _tracking) EndGesture(); + } + + void EndGesture() + { + _tracking = null; + if (_scrolling) + { + _inertia = default; + _scrolling = false; + _target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); + _gestureId = 0; + _lastMoveTimestamp = null; + } + + } + + + public void PointerReleased(PointerReleasedEventArgs e) + { + if (e.Pointer == _tracking && _scrolling) + { + e.Handled = true; + if (_inertia == default + || e.Timestamp == 0 + || _lastMoveTimestamp == 0 + || e.Timestamp - _lastMoveTimestamp > 200) + EndGesture(); + else + { + var savedGestureId = _gestureId; + var st = Stopwatch.StartNew(); + var lastTime = TimeSpan.Zero; + DispatcherTimer.Run(() => + { + // Another gesture has started, finish the current one + if (_gestureId != savedGestureId) + { + return false; + } + + var elapsedSinceLastTick = st.Elapsed - lastTime; + lastTime = st.Elapsed; + + var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); + var distance = speed * elapsedSinceLastTick.TotalSeconds; + _target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); + + + + if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd) + { + EndGesture(); + return false; + } + + return true; + }, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background); + } + } + } + } +} diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 23b0ad466e..65195394ab 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -18,6 +18,14 @@ namespace Avalonia.Input RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureEvent = + RoutedEvent.Register( + "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent ScrollGestureEndedEvent = + RoutedEvent.Register( + "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + private static WeakReference s_lastPress; static Gestures() diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 07e04486ec..7c687f0d7e 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Input.GestureRecognizers; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -127,6 +128,14 @@ namespace Avalonia.Input RoutedEvent.Register( "PointerReleased", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent PointerCaptureLostEvent = + RoutedEvent.Register( + "PointerCaptureLost", + RoutingStrategies.Direct); /// /// Defines the event. @@ -148,6 +157,7 @@ namespace Avalonia.Input private bool _isFocused; private bool _isPointerOver; + private GestureRecognizerCollection _gestureRecognizers; /// /// Initializes static members of the class. @@ -166,6 +176,7 @@ namespace Avalonia.Input PointerMovedEvent.AddClassHandler(x => x.OnPointerMoved); PointerPressedEvent.AddClassHandler(x => x.OnPointerPressed); PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); + PointerCaptureLostEvent.AddClassHandler(x => x.OnPointerCaptureLost); PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); @@ -263,6 +274,16 @@ namespace Avalonia.Input remove { RemoveHandler(PointerReleasedEvent, value); } } + /// + /// Occurs when the control or its child control loses the pointer capture for any reason, + /// event will not be triggered for a parent control if capture was transferred to another child of that parent control + /// + public event EventHandler PointerCaptureLost + { + add => AddHandler(PointerCaptureLostEvent, value); + remove => RemoveHandler(PointerCaptureLostEvent, value); + } + /// /// Occurs when the mouse wheen is scrolled over the control. /// @@ -370,6 +391,9 @@ namespace Avalonia.Input public List KeyBindings { get; } = new List(); + public GestureRecognizerCollection GestureRecognizers + => _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this)); + /// /// Focuses the control. /// @@ -460,6 +484,8 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerMoved(PointerEventArgs e) { + if (_gestureRecognizers?.HandlePointerMoved(e) == true) + e.Handled = true; } /// @@ -468,6 +494,8 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerPressed(PointerPressedEventArgs e) { + if (_gestureRecognizers?.HandlePointerPressed(e) == true) + e.Handled = true; } /// @@ -476,6 +504,17 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPointerReleased(PointerReleasedEventArgs e) { + if (_gestureRecognizers?.HandlePointerReleased(e) == true) + e.Handled = true; + } + + /// + /// Called before the event occurs. + /// + /// The event args. + protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e) + { + _gestureRecognizers?.HandlePointerCaptureLost(e); } /// diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index 90d9c37bd4..4c4d679087 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -14,18 +14,14 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - public class MouseDevice : IMouseDevice, IPointer + public class MouseDevice : IMouseDevice { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; - private IInputElement _captured; - private IDisposable _capturedSubscription; - PointerType IPointer.Type => PointerType.Mouse; - bool IPointer.IsPrimary => true; - int IPointer.Id { get; } = Pointer.GetNextFreeId(); - + private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -34,27 +30,9 @@ namespace Avalonia.Input /// within the control's bounds or not. To set the mouse capture, call the /// method. /// - public IInputElement Captured - { - get => _captured; - protected set - { - _capturedSubscription?.Dispose(); - _capturedSubscription = null; - - if (value != null) - { - _capturedSubscription = Observable.FromEventPattern( - x => value.DetachedFromVisualTree += x, - x => value.DetachedFromVisualTree -= x) - .Take(1) - .Subscribe(_ => Captured = null); - } + [Obsolete("Use IPointer instead")] + public IInputElement Captured => _pointer.Captured; - _captured = value; - } - } - /// /// Gets the mouse position, in screen coordinates. /// @@ -75,8 +53,7 @@ namespace Avalonia.Input /// public virtual void Capture(IInputElement control) { - // TODO: Check visibility and enabled state before setting capture. - Captured = control; + _pointer.Capture(control); } /// @@ -110,13 +87,13 @@ namespace Avalonia.Input if (rect.Contains(clientPoint)) { - if (Captured == null) + if (_pointer.Captured == null) { - SetPointerOver(this, root, clientPoint, InputModifiers.None); + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None); } else { - SetPointerOver(this, root, Captured, InputModifiers.None); + SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None); } } } @@ -144,13 +121,13 @@ namespace Avalonia.Input switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Root, e.InputModifiers); + LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers); break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); else e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); @@ -159,25 +136,25 @@ namespace Avalonia.Input case RawPointerEventType.RightButtonUp: case RawPointerEventType.MiddleButtonUp: if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); else - e.Handled = MouseUp(mouse, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); break; case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers); break; case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); break; } } - private void LeaveWindow(IMouseDevice device, IInputRoot root, InputModifiers inputModifiers) + private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); - ClearPointerOver(this, root, inputModifiers); + ClearPointerOver(this, timestamp, root, inputModifiers); } @@ -195,7 +172,7 @@ namespace Avalonia.Input rv.IsLeftButtonPressed = false; if (args.Type == RawPointerEventType.MiddleButtonUp) rv.IsMiddleButtonPressed = false; - if (args.Type == RawPointerEventType.RightButtonDown) + if (args.Type == RawPointerEventType.RightButtonUp) rv.IsRightButtonPressed = false; return rv; } @@ -212,8 +189,8 @@ namespace Avalonia.Input if (hit != null) { - IInteractive source = GetSource(hit); - + _pointer.Capture(hit); + var source = GetSource(hit); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); @@ -229,8 +206,7 @@ namespace Avalonia.Input _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); _lastMouseDownButton = properties.GetObsoleteMouseButton(); - var e = new PointerPressedEventArgs(source, this, root, p, properties, inputModifiers, _clickCount); - + var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; } @@ -239,7 +215,7 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties properties, + private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, InputModifiers inputModifiers) { Contract.Requires(device != null); @@ -247,24 +223,24 @@ namespace Avalonia.Input IInputElement source; - if (Captured == null) + if (_pointer.Captured == null) { - source = SetPointerOver(this, root, p, inputModifiers); + source = SetPointerOver(this, timestamp, root, p, inputModifiers); } else { - SetPointerOver(this, root, Captured, inputModifiers); - source = Captured; + SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers); + source = _pointer.Captured; } - var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root, - p, properties, inputModifiers); + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, + p, timestamp, properties, inputModifiers); source?.RaiseEvent(e); return e.Handled; } - private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties props, + private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, InputModifiers inputModifiers) { Contract.Requires(device != null); @@ -275,16 +251,18 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs(source, this, root, p, props, inputModifiers, _lastMouseDownButton); + var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, + _lastMouseDownButton); source?.RaiseEvent(e); + _pointer.Capture(null); return e.Handled; } return false; } - private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, + private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, Vector delta, InputModifiers inputModifiers) { @@ -296,7 +274,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerWheelEventArgs(source, this, root, p, props, inputModifiers, delta); + var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -309,7 +287,7 @@ namespace Avalonia.Input { Contract.Requires(hit != null); - return Captured ?? + return _pointer.Captured ?? (hit as IInteractive) ?? hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); } @@ -318,22 +296,22 @@ namespace Avalonia.Input { Contract.Requires(root != null); - return Captured ?? root.InputHitTest(p); + return _pointer.Captured ?? root.InputHitTest(p); } - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers) + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers) { - return new PointerEventArgs(ev, source, this, null, default, - new PointerPointProperties(inputModifiers), inputModifiers); + return new PointerEventArgs(ev, source, _pointer, null, default, + timestamp, new PointerPointProperties(inputModifiers), inputModifiers); } - private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers) + private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers); + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers); if (element!=null && !element.IsAttachedToVisualTree) { @@ -370,7 +348,7 @@ namespace Avalonia.Input } } - private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) + private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -381,18 +359,18 @@ namespace Avalonia.Input { if (element != null) { - SetPointerOver(device, root, element, inputModifiers); + SetPointerOver(device, timestamp, root, element, inputModifiers); } else { - ClearPointerOver(device, root, inputModifiers); + ClearPointerOver(device, timestamp, root, inputModifiers); } } return element; } - private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element, InputModifiers inputModifiers) + private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -414,7 +392,7 @@ namespace Avalonia.Input el = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, el, inputModifiers); + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers); if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index bdf2501b32..890ad57024 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input @@ -9,23 +11,40 @@ namespace Avalonia.Input private static int s_NextFreePointerId = 1000; public static int GetNextFreeId() => s_NextFreePointerId++; - public Pointer(int id, PointerType type, bool isPrimary, IInputElement implicitlyCaptured) + public Pointer(int id, PointerType type, bool isPrimary) { Id = id; Type = type; IsPrimary = isPrimary; - ImplicitlyCaptured = implicitlyCaptured; - if (ImplicitlyCaptured != null) - ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached; } public int Id { get; } + IInputElement FindCommonParent(IInputElement control1, IInputElement control2) + { + if (control1 == null || control2 == null) + return null; + var seen = new HashSet(control1.GetSelfAndVisualAncestors().OfType()); + return control2.GetSelfAndVisualAncestors().OfType().FirstOrDefault(seen.Contains); + } + public void Capture(IInputElement control) { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; + var oldCapture = control; Captured = control; + if (oldCapture != null) + { + var commonParent = FindCommonParent(control, oldCapture); + foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType()) + { + if (notifyTarget == commonParent) + break; + notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this)); + } + } + if (Captured != null) Captured.DetachedFromVisualTree += OnCaptureDetached; } @@ -38,26 +57,11 @@ namespace Avalonia.Input Capture(GetNextCapture(e.Parent)); } - private void OnImplicitCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) - { - ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached; - ImplicitlyCaptured = GetNextCapture(e.Parent); - if (ImplicitlyCaptured != null) - ImplicitlyCaptured.DetachedFromVisualTree += OnImplicitCaptureDetached; - } public IInputElement Captured { get; private set; } - public IInputElement ImplicitlyCaptured { get; private set; } - public IInputElement GetEffectiveCapture() => Captured ?? ImplicitlyCaptured; public PointerType Type { get; } public bool IsPrimary { get; } - public void Dispose() - { - if (ImplicitlyCaptured != null) - ImplicitlyCaptured.DetachedFromVisualTree -= OnImplicitCaptureDetached; - if (Captured != null) - Captured.DetachedFromVisualTree -= OnCaptureDetached; - } + public void Dispose() => Capture(null); } } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 1d07190a81..c827822192 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -17,7 +17,9 @@ namespace Avalonia.Input public PointerEventArgs(RoutedEvent routedEvent, IInteractive source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, + IVisual rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, InputModifiers modifiers) : base(routedEvent) { @@ -26,6 +28,7 @@ namespace Avalonia.Input _rootVisualPosition = rootVisualPosition; _properties = properties; Pointer = pointer; + Timestamp = timestamp; InputModifiers = modifiers; } @@ -50,6 +53,7 @@ namespace Avalonia.Input } public IPointer Pointer { get; } + public ulong Timestamp { get; } private IPointerDevice _device; @@ -86,11 +90,13 @@ namespace Avalonia.Input public PointerPressedEventArgs( IInteractive source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, + IVisual rootVisual, Point rootVisualPosition, + ulong timestamp, + PointerPointProperties properties, InputModifiers modifiers, int obsoleteClickCount = 1) - : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties, - modifiers) + : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) { _obsoleteClickCount = obsoleteClickCount; } @@ -105,10 +111,10 @@ namespace Avalonia.Input { public PointerReleasedEventArgs( IInteractive source, IPointer pointer, - IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers, - MouseButton obsoleteMouseButton) + IVisual rootVisual, Point rootVisualPosition, ulong timestamp, + PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton) : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, - properties, modifiers) + timestamp, properties, modifiers) { MouseButton = obsoleteMouseButton; } @@ -116,4 +122,15 @@ namespace Avalonia.Input [Obsolete()] public MouseButton MouseButton { get; private set; } } + + public class PointerCaptureLostEventArgs : RoutedEventArgs + { + public IPointer Pointer { get; } + + public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) + { + Pointer = pointer; + Source = source; + } + } } diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs index b409cc81bd..de1badfe96 100644 --- a/src/Avalonia.Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Input/PointerWheelEventArgs.cs @@ -11,9 +11,10 @@ namespace Avalonia.Input public Vector Delta { get; set; } public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, - Point rootVisualPosition, + Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, InputModifiers modifiers, Vector delta) - : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, properties, modifiers) + : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, + timestamp, properties, modifiers) { Delta = delta; } diff --git a/src/Avalonia.Input/Properties/AssemblyInfo.cs b/src/Avalonia.Input/Properties/AssemblyInfo.cs index 7025965f83..3a8d358931 100644 --- a/src/Avalonia.Input/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Input/Properties/AssemblyInfo.cs @@ -5,3 +5,4 @@ using System.Reflection; using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")] diff --git a/src/Avalonia.Input/ScrollGestureEventArgs.cs b/src/Avalonia.Input/ScrollGestureEventArgs.cs new file mode 100644 index 0000000000..a682e8f0a4 --- /dev/null +++ b/src/Avalonia.Input/ScrollGestureEventArgs.cs @@ -0,0 +1,29 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Input +{ + public class ScrollGestureEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Delta { get; } + private static int _nextId = 1; + + public static int GetNextFreeId() => _nextId++; + + public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + { + Id = id; + Delta = delta; + } + } + + public class ScrollGestureEndedEventArgs : RoutedEventArgs + { + public int Id { get; } + + public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + { + Id = id; + } + } +} diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index e9715bd87c..7f473bb320 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -35,28 +35,30 @@ namespace Avalonia.Input var hit = args.Root.InputHitTest(args.Position); _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), - PointerType.Touch, _pointers.Count == 0, hit); + PointerType.Touch, _pointers.Count == 0); + pointer.Capture(hit); } - var target = pointer.GetEffectiveCapture() ?? args.Root; + var target = pointer.Captured ?? args.Root; if (args.Type == RawPointerEventType.TouchBegin) { - var modifiers = GetModifiers(args.InputModifiers, false); target.RaiseEvent(new PointerPressedEventArgs(target, pointer, - args.Root, args.Position, new PointerPointProperties(modifiers), - modifiers)); + args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)), + GetModifiers(args.InputModifiers, false))); } if (args.Type == RawPointerEventType.TouchEnd) { _pointers.Remove(args.TouchPointId); - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); using (pointer) { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, - args.Root, args.Position, new PointerPointProperties(modifiers), - modifiers, pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); + args.Root, args.Position, ev.Timestamp, + new PointerPointProperties(GetModifiers(args.InputModifiers, false)), + GetModifiers(args.InputModifiers, pointer.IsPrimary), + pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); } } @@ -64,7 +66,7 @@ namespace Avalonia.Input { var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, - args.Position, new PointerPointProperties(modifiers), modifiers)); + args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers)); } } diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Avalonia.OpenGL/AngleOptions.cs new file mode 100644 index 0000000000..4b9c04f4e6 --- /dev/null +++ b/src/Avalonia.OpenGL/AngleOptions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL +{ + public class AngleOptions + { + public enum PlatformApi + { + DirectX9, + DirectX11 + } + + public List AllowedPlatformApis = new List + { + PlatformApi.DirectX9 + }; + } +} diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b14932acfe..b2b5a1a646 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Platform.Interop; using static Avalonia.OpenGL.EglConsts; @@ -13,21 +14,42 @@ namespace Avalonia.OpenGL private readonly int[] _contextAttributes; public IntPtr Handle => _display; + private AngleOptions.PlatformApi? _angleApi; public EglDisplay(EglInterface egl) { _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _egl.GetPlatformDisplayEXT != null) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - foreach (var dapi in new[] {EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE}) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if(_display != IntPtr.Zero) + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) @@ -64,29 +86,35 @@ namespace Avalonia.OpenGL if (!_egl.BindApi(cfg.Api)) continue; - var attribs = new[] + foreach(var stencilSize in new[]{8, 1, 0}) + foreach (var depthSize in new []{8, 1, 0}) { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, 8, - EGL_DEPTH_SIZE, 8, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - Type = cfg.Type; + var attribs = new[] + { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + _contextAttributes = cfg.Attributes; + Type = cfg.Type; + } } if (_contextAttributes == null) throw new OpenGlException("No suitable EGL config was found"); - + GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b)); } @@ -97,6 +125,7 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } + public EglInterface EglInterface => _egl; public IGlContext CreateContext(IGlContext share) { var shareCtx = (EglContext)share; diff --git a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs index f5dd413b0f..d2e4543af3 100644 --- a/src/Avalonia.OpenGL/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/EglGlPlatformSurface.cs @@ -26,31 +26,44 @@ namespace Avalonia.OpenGL public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() { var glSurface = _display.CreateWindowSurface(_info.Handle); - return new RenderTarget(_context, glSurface, _info); + return new RenderTarget(_display, _context, glSurface, _info); } - class RenderTarget : IGlPlatformSurfaceRenderTarget + class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { + private readonly EglDisplay _display; private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize _initialSize; - public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) + public RenderTarget(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) { + _display = display; _context = context; _glSurface = glSurface; _info = info; + _initialSize = info.Size; } public void Dispose() => _glSurface.Dispose(); + public bool IsCorrupted => _initialSize != _info.Size; + public IGlPlatformSurfaceRenderingSession BeginDraw() { var l = _context.Lock(); try { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); _context.MakeCurrent(_glSurface); - return new Session(_context, _glSurface, _info, l); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); + + return new Session(_display, _context, _glSurface, _info, l); } catch { @@ -61,15 +74,19 @@ namespace Avalonia.OpenGL class Session : IGlPlatformSurfaceRenderingSession { - private readonly IGlContext _context; + private readonly EglContext _context; private readonly EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; + private readonly EglDisplay _display; private IDisposable _lock; + - public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, + public Session(EglDisplay display, EglContext context, + EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info, IDisposable @lock) { _context = context; + _display = display; _glSurface = glSurface; _info = info; _lock = @lock; @@ -78,7 +95,11 @@ namespace Avalonia.OpenGL public void Dispose() { _context.Display.GlInterface.Flush(); + _display.EglInterface.WaitGL(); _glSurface.SwapBuffers(); + _display.EglInterface.WaitClient(); + _display.EglInterface.WaitGL(); + _display.EglInterface.WaitNative(); _context.Display.ClearContext(); _lock.Dispose(); } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 00fcd97af0..0a99778ddf 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Platform.Interop; @@ -15,13 +16,28 @@ namespace Avalonia.OpenGL { } + [DllImport("libegl.dll", CharSet = CharSet.Ansi)] + static extern IntPtr eglGetProcAddress(string proc); + static Func Load() { var os = AvaloniaLocator.Current.GetService().GetRuntimeInfo().OperatingSystem; if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android) return Load("libEGL.so.1"); if (os == OperatingSystemType.WinNT) - return Load(@"libegl.dll"); + { + var disp = eglGetProcAddress("eglGetPlatformDisplayEXT"); + if (disp == IntPtr.Zero) + throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point"); + return (name, optional) => + { + var r = eglGetProcAddress(name); + if (r == IntPtr.Zero && !optional) + throw new OpenGlException($"Entry point {r} is not found"); + return r; + }; + } + throw new PlatformNotSupportedException(); } @@ -91,6 +107,31 @@ namespace Avalonia.OpenGL [GlEntryPoint("eglGetConfigAttrib")] public EglGetConfigAttrib GetConfigAttrib { get; } + public delegate bool EglWaitGL(); + [GlEntryPoint("eglWaitGL")] + public EglWaitGL WaitGL { get; } + + public delegate bool EglWaitClient(); + [GlEntryPoint("eglWaitClient")] + public EglWaitGL WaitClient { get; } + + public delegate bool EglWaitNative(); + [GlEntryPoint("eglWaitNative")] + public EglWaitGL WaitNative { get; } + + public delegate IntPtr EglQueryString(IntPtr display, int i); + + [GlEntryPoint("eglQueryString")] + public EglQueryString QueryStringNative { get; } + + public string QueryString(IntPtr display, int i) + { + var rv = QueryStringNative(display, i); + if (rv == IntPtr.Zero) + return null; + return Marshal.PtrToStringAnsi(rv); + } + // ReSharper restore UnassignedGetOnlyAutoProperty } } diff --git a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs index 53da93315c..d198d46e5c 100644 --- a/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs @@ -6,4 +6,9 @@ namespace Avalonia.OpenGL { IGlPlatformSurfaceRenderingSession BeginDraw(); } -} \ No newline at end of file + + public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget + { + bool IsCorrupted { get; } + } +} diff --git a/src/Avalonia.Themes.Default/ScrollViewer.xaml b/src/Avalonia.Themes.Default/ScrollViewer.xaml index 63440921d6..3e130cad67 100644 --- a/src/Avalonia.Themes.Default/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/ScrollViewer.xaml @@ -12,7 +12,14 @@ Extent="{TemplateBinding Extent, Mode=TwoWay}" Margin="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" - Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/> + Viewport="{TemplateBinding Viewport, Mode=TwoWay}"> + + + + - \ No newline at end of file + diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs index 522de64ec7..516bea782e 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -23,4 +23,9 @@ namespace Avalonia.Platform /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer); } + + public interface IRenderTargetWithCorruptionInfo : IRenderTarget + { + bool IsCorrupted { get; } + } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index c83a8436b4..0d077d2a3a 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -245,6 +245,11 @@ namespace Avalonia.Rendering { if (context != null) return context; + if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + RenderTarget.Dispose(); + RenderTarget = null; + } if (RenderTarget == null) RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); return context = RenderTarget.CreateDrawingContext(this); diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 75d8f036d6..2d4a39e026 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -7,11 +7,25 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); + + /// + /// Tries to lock the target surface or window + /// + /// IDisposable if succeeded to obtain the lock public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) return Disposable.Create(() => Monitor.Exit(_lock)); return null; } + + /// + /// Enters a waiting lock, only use from platform code, not from the renderer + /// + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => Monitor.Exit(_lock)); + } } } diff --git a/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs new file mode 100644 index 0000000000..dd6cf7ad15 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/UiThreadRenderTimer.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace Avalonia.Rendering +{ + /// + /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms + /// + + public class UiThreadRenderTimer : DefaultRenderTimer + { + public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) + { + } + + protected override IDisposable StartCore(Action tick) + { + bool cancelled = false; + var st = Stopwatch.StartNew(); + DispatcherTimer.Run(() => + { + if (cancelled) + return false; + tick(st.Elapsed); + return !cancelled; + }, TimeSpan.FromSeconds(1.0 / FramesPerSecond), DispatcherPriority.Render); + return Disposable.Create(() => cancelled = true); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index a73c523483..610cda30c6 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit a73c5234831267b23160e01a9fbc83be633f69fc +Subproject commit 610cda30c69e32e83c8235060606480904c937bc diff --git a/src/Skia/Avalonia.Skia/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/GlRenderTarget.cs index 7c0c42ca37..a7c1d0a38b 100644 --- a/src/Skia/Avalonia.Skia/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/GlRenderTarget.cs @@ -8,7 +8,7 @@ using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - internal class GlRenderTarget : IRenderTarget + internal class GlRenderTarget : IRenderTargetWithCorruptionInfo { private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; @@ -21,6 +21,8 @@ namespace Avalonia.Skia public void Dispose() => _surface.Dispose(); + public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { var session = _surface.BeginDraw(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 7f07f36de8..9e8b2d58a6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -33,6 +33,7 @@ namespace Avalonia.Win32 private bool _multitouch; private TouchDevice _touchDevice = new TouchDevice(); private IInputRoot _owner; + private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock(); private bool _trackingMouse; private bool _decorated = true; private bool _resizable = true; @@ -150,7 +151,9 @@ namespace Avalonia.Win32 if (customRendererFactory != null) return customRendererFactory.Create(root, loop); - return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root); + return Win32Platform.UseDeferredRendering ? + (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) : + new ImmediateRenderer(root); } public void Resize(Size value) @@ -634,8 +637,6 @@ namespace Avalonia.Win32 { foreach (var touchInput in touchInputs) { - var pt = new POINT {X = touchInput.X / 100, Y = touchInput.Y / 100}; - UnmanagedMethods.ScreenToClient(_hwnd, ref pt); Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, _owner, touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_UP) ? @@ -643,7 +644,7 @@ namespace Avalonia.Win32 touchInput.Flags.HasFlag(TouchInputFlags.TOUCHEVENTF_DOWN) ? RawPointerEventType.TouchBegin : RawPointerEventType.TouchUpdate, - new Point(pt.X, pt.Y), + PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), WindowsKeyboardDevice.Instance.Modifiers, touchInput.Id)); } @@ -667,18 +668,26 @@ namespace Avalonia.Win32 break; case UnmanagedMethods.WindowsMessage.WM_PAINT: - UnmanagedMethods.PAINTSTRUCT ps; - if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + using (_rendererLock.Lock()) { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, (r.bottom - r.top) / f)); - UnmanagedMethods.EndPaint(_hwnd, ref ps); + UnmanagedMethods.PAINTSTRUCT ps; + if (UnmanagedMethods.BeginPaint(_hwnd, out ps) != IntPtr.Zero) + { + var f = Scaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + UnmanagedMethods.EndPaint(_hwnd, ref ps); + } } return IntPtr.Zero; case UnmanagedMethods.WindowsMessage.WM_SIZE: + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } var size = (UnmanagedMethods.SizeCommand)wParam; if (Resized != null && @@ -744,7 +753,8 @@ namespace Avalonia.Win32 } } - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + using (_rendererLock.Lock()) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } static InputModifiers GetMouseModifiers(IntPtr wParam) diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs deleted file mode 100644 index 93163f4a92..0000000000 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class GridLayoutTests - { - private const double Inf = double.PositiveInfinity; - - [Theory] - [InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] - public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] - public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] - [InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] - [InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] - public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] - [InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] - [InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] - public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - - /// - /// This is needed because Mono somehow converts double array to object array in attribute metadata - /// - static void AssertEqual(IList expected, IReadOnlyList actual) - { - var conv = expected.Cast().ToArray(); - Assert.Equal(conv, actual); - } - - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] - private static void TestRowDefinitionsOnly(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] - [InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] - [InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] - public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, - IList expectedMeasureList, IList expectedArrangeList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(Inf); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(measure.DesiredLength, measure); - AssertEqual(expectedArrangeList, arrange.LengthList); - } - - [Theory] - [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] - public void MeasureArrange_ChildHasSize_Correct(string length, - IList childLengthList, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), - x => (double)childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] - [InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] - [InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] - [InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] - [InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] - public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( - double containerLength, double expectedDesiredLength, - IList expectedMeasureLengthList, IList expectedArrangeLengthList) - { - var length = "100,*,2*"; - var childLengthList = new[] { 150d, 150d, 150d }; - var spans = new[] { 1, 2, 1 }; - - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), - x => childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange( - double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, - measure); - AssertEqual(expectedArrangeLengthList, arrange.LengthList); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 5799cb91c4..df804d5d8c 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,12 +1,73 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; using Xunit; +using Xunit.Abstractions; namespace Avalonia.Controls.UnitTests { public class GridTests { + private readonly ITestOutputHelper output; + + public GridTests(ITestOutputHelper output) + { + this.output = output; + } + + private Grid CreateGrid(params (string name, GridLength width)[] columns) + { + return CreateGrid(columns.Select(c => + (c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns) + { + return CreateGrid(columns.Select(c => + (c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); + } + + private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) + { + + var grid = new Grid(); + foreach (var k in columns.Select(c => new ColumnDefinition + { + SharedSizeGroup = c.name, + Width = c.width, + MinWidth = c.minWidth, + MaxWidth = c.maxWidth + })) + grid.ColumnDefinitions.Add(k); + + return grid; + } + + private Control AddSizer(Grid grid, int column, double size = 30) + { + var ctrl = new Control { MinWidth = size, MinHeight = size }; + ctrl.SetValue(Grid.ColumnProperty, column); + grid.Children.Add(ctrl); + output.WriteLine($"[AddSizer] Column: {column} MinWidth: {size} MinHeight: {size}"); + return ctrl; + } + + private void PrintColumnDefinitions(Grid grid) + { + output.WriteLine($"[Grid] ActualWidth: {grid.Bounds.Width} ActualHeight: {grid.Bounds.Width}"); + output.WriteLine($"[ColumnDefinitions]"); + for (int i = 0; i < grid.ColumnDefinitions.Count; i++) + { + var cd = grid.ColumnDefinitions[i]; + output.WriteLine($"[{i}] ActualWidth: {cd.ActualWidth} SharedSizeGroup: {cd.SharedSizeGroup}"); + } + } + [Fact] public void Calculates_Colspan_Correctly() { @@ -180,5 +241,1121 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } + [Fact] + public void Grid_GridLength_Same_Size_Pixel_0() + { + var grid = CreateGrid( + (null, new GridLength()), + (null, new GridLength()), + (null, new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Grid_GridLength_Same_Size_Pixel_50() + { + var grid = CreateGrid( + (null, new GridLength(50)), + (null, new GridLength(50)), + (null, new GridLength(50)), + (null, new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void Grid_GridLength_Same_Size_Auto() + { + var grid = CreateGrid( + (null, new GridLength(0, GridUnitType.Auto)), + (null, new GridLength(0, GridUnitType.Auto)), + (null, new GridLength(0, GridUnitType.Auto)), + (null, new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Grid_GridLength_Same_Size_Star() + { + var grid = CreateGrid( + (null, new GridLength(1, GridUnitType.Star)), + (null, new GridLength(1, GridUnitType.Star)), + (null, new GridLength(1, GridUnitType.Star)), + (null, new GridLength(1, GridUnitType.Star))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50() + { + var grid = CreateGrid( + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_Last_Column_0() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_And_Last_Column_0() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star))); // Star sizing is treated as Auto, 1 is ignored + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_0_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength()), + ("B", new GridLength()), + ("B", new GridLength()), + ("A", new GridLength()), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Pixel_50_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Auto_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("B", new GridLength(0, GridUnitType.Auto)), + ("A", new GridLength(0, GridUnitType.Auto)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void SharedSize_Grid_GridLength_Same_Size_Star_First_And_Last_Column_0_Two_Groups() + { + var grid = CreateGrid( + (null, new GridLength()), + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("B", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + ("A", new GridLength(1, GridUnitType.Star)), // Star sizing is treated as Auto, 1 is ignored + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Size_Propagation_Is_Constrained_To_Innermost_Scope() + { + var grids = new[] { CreateGrid(("A", new GridLength())), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var innerScope = new Grid(); + + foreach (var grid in grids) + innerScope.Children.Add(grid); + + innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); + + var outerGrid = CreateGrid(("A", new GridLength(0))); + var outerScope = new Grid(); + outerScope.Children.Add(outerGrid); + outerScope.Children.Add(innerScope); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(outerScope); + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Size_Group_Changes_Are_Tracked() + { + var grids = new[] { + CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), + CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var scope = new Grid(); + foreach (var xgrids in grids) + scope.Children.Add(xgrids); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + PrintColumnDefinitions(grids[0]); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; + + root.Measure(new Size(51, 51)); + root.Arrange(new Rect(new Point(), new Point(51, 51))); + PrintColumnDefinitions(grids[0]); + Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = null; + + root.Measure(new Size(52, 52)); + root.Arrange(new Rect(new Point(), new Point(52, 52))); + PrintColumnDefinitions(grids[0]); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Collection_Changes_Are_Tracked() + { + var grid = CreateGrid( + ("A", new GridLength(20)), + ("A", new GridLength(30)), + ("A", new GridLength(40)), + (null, new GridLength())); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth)); + + grid.ColumnDefinitions.RemoveAt(2); + + // NOTE: THIS IS BROKEN IN WPF + // grid.Measure(new Size(200, 200)); + // grid.Arrange(new Rect(new Point(), new Point(200, 200))); + // PrintColumnDefinitions(grid); + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(30), SharedSizeGroup = "A" }); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); + + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; + + // NOTE: THIS IS BROKEN IN WPF + // grid.Measure(new Size(200, 200)); + // grid.Arrange(new Rect(new Point(), new Point(200, 200))); + // PrintColumnDefinitions(grid); + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void Size_Priorities_Are_Maintained() + { + var sizers = new List(); + var grid = CreateGrid( + ("A", new GridLength(20)), + ("A", new GridLength(20, GridUnitType.Auto)), + ("A", new GridLength(1, GridUnitType.Star)), + ("A", new GridLength(1, GridUnitType.Star)), + (null, new GridLength())); + for (int i = 0; i < 3; i++) + sizers.Add(AddSizer(grid, i, 6 + i * 6)); + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + PrintColumnDefinitions(grid); + // all in group are equal to the first fixed column + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth)); + + grid.ColumnDefinitions[0].SharedSizeGroup = null; + + grid.Measure(new Size(100, 100)); + grid.Arrange(new Rect(new Point(), new Point(100, 100))); + PrintColumnDefinitions(grid); + + // NOTE: THIS IS BROKEN IN WPF + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth)); + // grid.ColumnDefinitions[1].SharedSizeGroup = null; + + // grid.Measure(new Size(100, 100)); + // grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // PrintColumnDefinitions(grid); + + // NOTE: THIS IS BROKEN IN WPF + // all in group are equal to width (MinWidth) of the sizer in the second column + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); + + // NOTE: THIS IS BROKEN IN WPF + // grid.ColumnDefinitions[2].SharedSizeGroup = null; + + // NOTE: THIS IS BROKEN IN WPF + // grid.Measure(new Size(double.PositiveInfinity, 100)); + // grid.Arrange(new Rect(new Point(), new Point(100, 100))); + // PrintColumnDefinitions(grid); + // with no constraint star columns default to the MinWidth of the sizer in the column + // Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(0, cd.ActualWidth)); + } + + [Fact] + public void ColumnDefinitions_Collection_Is_ReadOnly() + { + var grid = CreateGrid( + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50)), + ("A", new GridLength(50))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); + + grid.ColumnDefinitions[0] = new ColumnDefinition { Width = new GridLength(25), SharedSizeGroup = "A" }; + grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(75), SharedSizeGroup = "B" }; + grid.ColumnDefinitions[2] = new ColumnDefinition { Width = new GridLength(75), SharedSizeGroup = "B" }; + grid.ColumnDefinitions[3] = new ColumnDefinition { Width = new GridLength(25), SharedSizeGroup = "A" }; + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + } + + [Fact] + public void ColumnDefinitions_Collection_Reset_SharedSizeGroup() + { + var grid = CreateGrid( + ("A", new GridLength(25)), + ("B", new GridLength(75)), + ("B", new GridLength(75)), + ("A", new GridLength(25))); + + var scope = new Grid(); + scope.Children.Add(grid); + + var root = new Grid(); + root.UseLayoutRounding = false; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Children.Add(scope); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(25, cd.ActualWidth)); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "B"), cd => Assert.Equal(75, cd.ActualWidth)); + + grid.ColumnDefinitions[0].SharedSizeGroup = null; + grid.ColumnDefinitions[0].Width = new GridLength(50); + grid.ColumnDefinitions[1].SharedSizeGroup = null; + grid.ColumnDefinitions[1].Width = new GridLength(50); + grid.ColumnDefinitions[2].SharedSizeGroup = null; + grid.ColumnDefinitions[2].Width = new GridLength(50); + grid.ColumnDefinitions[3].SharedSizeGroup = null; + grid.ColumnDefinitions[3].Width = new GridLength(50); + + grid.Measure(new Size(200, 200)); + grid.Arrange(new Rect(new Point(), new Point(200, 200))); + PrintColumnDefinitions(grid); + Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == null), cd => Assert.Equal(50, cd.ActualWidth)); + } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs index d6542d23f0..373bbaed75 100644 --- a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs @@ -1,3 +1,4 @@ +using System.Reactive; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -6,22 +7,9 @@ namespace Avalonia.Controls.UnitTests { public class MouseTestHelper { - - class TestPointer : IPointer - { - public int Id { get; } = Pointer.GetNextFreeId(); - - public void Capture(IInputElement control) - { - Captured = control; - } - - public IInputElement Captured { get; set; } - public PointerType Type => PointerType.Mouse; - public bool IsPrimary => true; - } - - TestPointer _pointer = new TestPointer(); + private Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + private ulong _nextStamp = 1; + private ulong Timestamp() => _nextStamp++; private InputModifiers _pressedButtons; public IInputElement Captured => _pointer.Captured; @@ -49,8 +37,10 @@ namespace Avalonia.Controls.UnitTests public void Down(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, InputModifiers modifiers = default, int clickCount = 1) - => Down(target, target, mouseButton, position, modifiers, clickCount); - + { + Down(target, target, mouseButton, position, modifiers, clickCount); + } + public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, Point position = default, InputModifiers modifiers = default, int clickCount = 1) { @@ -61,7 +51,8 @@ namespace Avalonia.Controls.UnitTests else { _pressedButton = mouseButton; - target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props, + _pointer.Capture((IInputElement)target); + target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props, GetModifiers(modifiers), clickCount)); } } @@ -70,7 +61,7 @@ namespace Avalonia.Controls.UnitTests public void Move(IInteractive target, IInteractive source, in Point position, InputModifiers modifiers = default) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (IVisual)target, position, - new PointerPointProperties(_pressedButtons), GetModifiers(modifiers))); + Timestamp(), new PointerPointProperties(_pressedButtons), GetModifiers(modifiers))); } public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, @@ -84,8 +75,12 @@ namespace Avalonia.Controls.UnitTests _pressedButtons = (_pressedButtons | conv) ^ conv; var props = new PointerPointProperties(_pressedButtons); if (ButtonCount(props) == 0) - target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, props, + { + _pointer.Capture(null); + target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, + Timestamp(), props, GetModifiers(modifiers), _pressedButton)); + } else Move(target, source, position); } @@ -103,13 +98,13 @@ namespace Avalonia.Controls.UnitTests public void Enter(IInteractive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, - new PointerPointProperties(_pressedButtons), _pressedButtons)); + Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); } public void Leave(IInteractive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, - new PointerPointProperties(_pressedButtons), _pressedButtons)); + Timestamp(), new PointerPointProperties(_pressedButtons), _pressedButtons)); } } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index fb3a5bfefb..ba4d6ca9c5 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,14 +11,14 @@ namespace Avalonia.Controls.UnitTests.Platform public class DefaultMenuInteractionHandlerTests { static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) - => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default); + => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, 0, new PointerPointProperties(), default); static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source, - new FakePointer(), (IVisual)source, default, new PointerPointProperties {IsLeftButtonPressed = true}, + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties {IsLeftButtonPressed = true}, default); static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, - new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default, MouseButton.Left); + new FakePointer(), (IVisual)source, default,0, new PointerPointProperties(), default, MouseButton.Left); public class TopLevel { diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs deleted file mode 100644 index 715e9da880..0000000000 --- a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Primitives; -using Avalonia.Input; -using Avalonia.Platform; -using Avalonia.UnitTests; - -using Moq; - -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class SharedSizeScopeTests - { - public SharedSizeScopeTests() - { - } - - [Fact] - public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope() - { - var grids = new[] { new Grid(), new Grid(), new Grid() }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); - } - - [Fact] - public void All_Descendant_Grids_Are_Registered_When_Setting_Scope() - { - var grids = new[] { new Grid(), new Grid(), new Grid() }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.Child = scope; - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - - Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); - } - - [Fact] - public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope() - { - var grids = new[] { new Grid(), new Grid(), new Grid() }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); - root.SetValue(Grid.IsSharedSizeScopeProperty, false); - Assert.All(grids, g => Assert.False(g.HasSharedSizeScope())); - Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty)); - } - - [Fact] - public void Size_Is_Propagated_Between_Grids() - { - var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))}; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); - } - - [Fact] - public void Size_Propagation_Is_Constrained_To_Innermost_Scope() - { - var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; - var innerScope = new Panel(); - innerScope.Children.AddRange(grids); - innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); - - var outerGrid = CreateGrid(("A", new GridLength(0))); - var outerScope = new Panel(); - outerScope.Children.AddRange(new[] { outerGrid, innerScope }); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = outerScope; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); - } - - [Fact] - public void Size_Is_Propagated_Between_Rows_And_Columns() - { - var grid = new Grid - { - ColumnDefinitions = new ColumnDefinitions("*,30"), - RowDefinitions = new RowDefinitions("*,10") - }; - - grid.ColumnDefinitions[1].SharedSizeGroup = "A"; - grid.RowDefinitions[1].SharedSizeGroup = "A"; - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = grid; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(30, grid.RowDefinitions[1].ActualHeight); - } - - [Fact] - public void Size_Group_Changes_Are_Tracked() - { - var grids = new[] { - CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), - CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; - var scope = new Panel(); - scope.Children.AddRange(grids); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); - - grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; - - root.Measure(new Size(51, 51)); - root.Arrange(new Rect(new Point(), new Point(51, 51))); - Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); - - grids[0].ColumnDefinitions[0].SharedSizeGroup = null; - - root.Measure(new Size(52, 52)); - root.Arrange(new Rect(new Point(), new Point(52, 52))); - Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); - } - - [Fact] - public void Collection_Changes_Are_Tracked() - { - var grid = CreateGrid( - ("A", new GridLength(20)), - ("A", new GridLength(30)), - ("A", new GridLength(40)), - (null, new GridLength())); - - var scope = new Panel(); - scope.Children.Add(grid); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(40, cd.ActualWidth)); - - grid.ColumnDefinitions.RemoveAt(2); - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); - - grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" }); - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(35, cd.ActualWidth)); - - grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(30, cd.ActualWidth)); - - grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; - - grid.Measure(new Size(200, 200)); - grid.Arrange(new Rect(new Point(), new Point(200, 200))); - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(50, cd.ActualWidth)); - } - - [Fact] - public void Size_Priorities_Are_Maintained() - { - var sizers = new List(); - var grid = CreateGrid( - ("A", new GridLength(20)), - ("A", new GridLength(20, GridUnitType.Auto)), - ("A", new GridLength(1, GridUnitType.Star)), - ("A", new GridLength(1, GridUnitType.Star)), - (null, new GridLength())); - for (int i = 0; i < 3; i++) - sizers.Add(AddSizer(grid, i, 6 + i * 6)); - var scope = new Panel(); - scope.Children.Add(grid); - - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; - - grid.Measure(new Size(100, 100)); - grid.Arrange(new Rect(new Point(), new Point(100, 100))); - // all in group are equal to the first fixed column - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(20, cd.ActualWidth)); - - grid.ColumnDefinitions[0].SharedSizeGroup = null; - - grid.Measure(new Size(100, 100)); - grid.Arrange(new Rect(new Point(), new Point(100, 100))); - // all in group are equal to width (MinWidth) of the sizer in the second column - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 1 * 6, cd.ActualWidth)); - - grid.ColumnDefinitions[1].SharedSizeGroup = null; - - grid.Measure(new Size(double.PositiveInfinity, 100)); - grid.Arrange(new Rect(new Point(), new Point(100, 100))); - // with no constraint star columns default to the MinWidth of the sizer in the column - Assert.All(grid.ColumnDefinitions.Where(cd => cd.SharedSizeGroup == "A"), cd => Assert.Equal(6 + 2 * 6, cd.ActualWidth)); - } - - // grid creators - private Grid CreateGrid(params string[] columnGroups) - { - return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } - - private Grid CreateGrid(params (string name, GridLength width)[] columns) - { - return CreateGrid(columns.Select(c => - (c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } - - private Grid CreateGrid(params (string name, GridLength width, double minWidth)[] columns) - { - return CreateGrid(columns.Select(c => - (c.name, c.width, c.minWidth, ColumnDefinition.MaxWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } - - private Grid CreateGrid(params (string name, GridLength width, double minWidth, double maxWidth)[] columns) - { - var columnDefinitions = new ColumnDefinitions(); - - columnDefinitions.AddRange( - columns.Select(c => new ColumnDefinition - { - SharedSizeGroup = c.name, - Width = c.width, - MinWidth = c.minWidth, - MaxWidth = c.maxWidth - }) - ); - var grid = new Grid - { - ColumnDefinitions = columnDefinitions - }; - - return grid; - } - - private Control AddSizer(Grid grid, int column, double size = 30) - { - var ctrl = new Control { MinWidth = size, MinHeight = size }; - ctrl.SetValue(Grid.ColumnProperty,column); - grid.Children.Add(ctrl); - return ctrl; - } - } -} diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 8f1c071695..983f541c2a 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -15,7 +15,7 @@ namespace Avalonia.Input.UnitTests public class MouseDeviceTests { [Fact] - public void Capture_Is_Cleared_When_Control_Removed() + public void Capture_Is_Transferred_To_Parent_When_Control_Removed() { Canvas control; var root = new TestRoot @@ -29,7 +29,7 @@ namespace Avalonia.Input.UnitTests root.Child = null; - Assert.Null(target.Captured); + Assert.Same(root, target.Captured); } [Fact] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 3511919e39..5e346e5289 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -158,6 +158,55 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal("321", loaded.Test); } + + void AssertThrows(Action callback, Func check) + { + try + { + callback(); + } + catch (Exception e) when (check(e)) + { + return; + } + + throw new Exception("Expected exception was not thrown"); + } + + public static object SomeStaticProperty { get; set; } + + [Fact] + public void Bug2570() + { + SomeStaticProperty = "123"; + AssertThrows(() => new AvaloniaXamlLoader() {IsDesignMode = true} + .Load(@" +", typeof(XamlIlTests).Assembly), + e => e.Message.Contains("Unable to resolve ") + && e.Message.Contains(" as static field, property, constant or enum value")); + + } + + [Fact] + public void Design_Mode_DataContext_Should_Be_Set() + { + SomeStaticProperty = "123"; + + var loaded = (UserControl)new AvaloniaXamlLoader() {IsDesignMode = true} + .Load(@" +", typeof(XamlIlTests).Assembly); + Assert.Equal(Design.GetDataContext(loaded), SomeStaticProperty); + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window @@ -188,7 +237,7 @@ namespace Avalonia.Markup.Xaml.UnitTests ((ItemsControl)Content).Items = new[] {"123"}; } } - + public class XamlIlClassWithCustomProperty : UserControl { public string Test { get; set; }