From e123a737cc72d11c649c9989229a8851cfc769d3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 14:55:20 +0200 Subject: [PATCH 001/179] Added failing tests for #2501 --- .../ButtonTests.cs | 15 ++++++++++++ .../MenuItemTests.cs | 24 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9a751d4953..d1872c5b9e 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -31,6 +31,21 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsEnabled); } + [Fact] + public void Button_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new Button + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEnabledCore); + } + [Fact] public void Button_Is_Disabled_When_Bound_Command_Doesnt_Exist() { diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 32d154249c..704c79155a 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; +using Avalonia.Input; using Avalonia.UnitTests; using Xunit; @@ -58,10 +59,31 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, command.SubscriptionCount); } + [Fact] + public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new MenuItem + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEnabledCore); + } + private class TestCommand : ICommand { + private bool _enabled; private EventHandler _canExecuteChanged; + public TestCommand(bool enabled = true) + { + _enabled = enabled; + } + public int SubscriptionCount { get; private set; } public event EventHandler CanExecuteChanged @@ -70,7 +92,7 @@ namespace Avalonia.Controls.UnitTests remove { _canExecuteChanged -= value; --SubscriptionCount; } } - public bool CanExecute(object parameter) => true; + public bool CanExecute(object parameter) => _enabled; public void Execute(object parameter) { From e6be9b7c5acf1c26d9f519707ac631bb98fe0984 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 15:09:13 +0200 Subject: [PATCH 002/179] Renamed IsEnabledCore -> IsEffectivelyEnabled. I now understand how WPF's `IsEnabledCore` works, and it's not like this. Rename `IsEnabledCore` to `IsEffectivelyEnabled` so that we can add a new `IsEnabledCore` property which works like WPF's. This also aligns with the existing `IsEffectivelyVisible` property. --- src/Avalonia.Controls/ComboBox.cs | 2 +- src/Avalonia.Input/FocusManager.cs | 2 +- src/Avalonia.Input/IInputElement.cs | 6 +-- src/Avalonia.Input/InputElement.cs | 42 +++++++++---------- src/Avalonia.Input/InputExtensions.cs | 2 +- .../Navigation/FocusExtensions.cs | 4 +- .../ButtonTests.cs | 2 +- .../MenuItemTests.cs | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index bf79e192c5..5d427df5a6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -302,7 +302,7 @@ namespace Avalonia.Controls } } - private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible; + private bool CanFocus(IControl control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible; private void UpdateSelectionBoxItem(object item) { diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 102da6efc4..1603b250b8 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -146,7 +146,7 @@ namespace Avalonia.Input /// /// The element. /// True if the element can be focused. - private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; /// /// Gets the focus scope ancestors of the specified control, traversing popups. diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs index c9924dbffb..9247fb48a9 100644 --- a/src/Avalonia.Input/IInputElement.cs +++ b/src/Avalonia.Input/IInputElement.cs @@ -83,14 +83,14 @@ namespace Avalonia.Input Cursor Cursor { get; } /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. + /// Gets a value indicating whether this control and all its parents are enabled. /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - bool IsEnabledCore { get; } + bool IsEffectivelyEnabled { get; } /// /// Gets a value indicating whether the control is focused. diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 07e04486ec..a1b00f47fb 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -27,10 +27,10 @@ namespace Avalonia.Input AvaloniaProperty.Register(nameof(IsEnabled), true); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty IsEnabledCoreProperty = - AvaloniaProperty.Register(nameof(IsEnabledCore), true); + public static readonly StyledProperty IsEffectivelyEnabledProperty = + AvaloniaProperty.Register(nameof(IsEffectivelyEnabled), true); /// /// Gets or sets associated mouse cursor. @@ -168,7 +168,7 @@ namespace Avalonia.Input PointerReleasedEvent.AddClassHandler(x => x.OnPointerReleased); PointerWheelChangedEvent.AddClassHandler(x => x.OnPointerWheelChanged); - PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); + PseudoClass(IsEffectivelyEnabledProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); } @@ -349,23 +349,23 @@ namespace Avalonia.Input /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - bool IInputElement.IsEnabledCore => IsEnabledCore; + bool IInputElement.IsEffectivelyEnabled => IsEffectivelyEnabled; /// /// Gets a value indicating whether the control is effectively enabled for user interaction. /// /// /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the + /// controls. The property takes into account the /// value of this control and its parent controls. /// - protected bool IsEnabledCore + protected bool IsEffectivelyEnabled { - get { return GetValue(IsEnabledCoreProperty); } - set { SetValue(IsEnabledCoreProperty, value); } + get { return GetValue(IsEffectivelyEnabledProperty); } + set { SetValue(IsEffectivelyEnabledProperty, value); } } public List KeyBindings { get; } = new List(); @@ -393,7 +393,7 @@ namespace Avalonia.Input protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTreeCore(e); - UpdateIsEnabledCore(); + UpdateIsEffectivelyEnabled(); } /// @@ -488,7 +488,7 @@ namespace Avalonia.Input private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) { - ((InputElement)e.Sender).UpdateIsEnabledCore(); + ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); } /// @@ -512,32 +512,32 @@ namespace Avalonia.Input } /// - /// Updates the property value. + /// Updates the property value. /// - private void UpdateIsEnabledCore() + private void UpdateIsEffectivelyEnabled() { - UpdateIsEnabledCore(this.GetVisualParent()); + UpdateIsEffectivelyEnabled(this.GetVisualParent()); } /// - /// Updates the property based on the parent's - /// . + /// Updates the property based on the parent's + /// . /// /// The parent control. - private void UpdateIsEnabledCore(InputElement parent) + private void UpdateIsEffectivelyEnabled(InputElement parent) { if (parent != null) { - IsEnabledCore = IsEnabled && parent.IsEnabledCore; + IsEffectivelyEnabled = IsEnabled && parent.IsEffectivelyEnabled; } else { - IsEnabledCore = IsEnabled; + IsEffectivelyEnabled = IsEnabled; } foreach (var child in this.GetVisualChildren().OfType()) { - child.UpdateIsEnabledCore(this); + child.UpdateIsEffectivelyEnabled(this); } } } diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index f184e41998..c1d0729560 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -45,7 +45,7 @@ namespace Avalonia.Input return element != null && element.IsVisible && element.IsHitTestVisible && - element.IsEnabledCore && + element.IsEffectivelyEnabled && element.IsAttachedToVisualTree; } } diff --git a/src/Avalonia.Input/Navigation/FocusExtensions.cs b/src/Avalonia.Input/Navigation/FocusExtensions.cs index 41e7c4cd7b..794dc63f84 100644 --- a/src/Avalonia.Input/Navigation/FocusExtensions.cs +++ b/src/Avalonia.Input/Navigation/FocusExtensions.cs @@ -13,13 +13,13 @@ namespace Avalonia.Input.Navigation /// /// The element. /// True if the element can be focused. - public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible; + public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible; /// /// Checks if descendants of the specified element can be focused. /// /// The element. /// True if descendants of the element can be focused. - public static bool CanFocusDescendants(this IInputElement e) => e.IsEnabledCore && e.IsVisible; + public static bool CanFocusDescendants(this IInputElement e) => e.IsEffectivelyEnabled && e.IsVisible; } } diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index d1872c5b9e..9255b00e50 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -43,7 +43,7 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = target }; - Assert.False(((IInputElement)target).IsEnabledCore); + Assert.False(((IInputElement)target).IsEffectivelyEnabled); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 704c79155a..ebf2c72ab4 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -71,7 +71,7 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = target }; - Assert.False(((IInputElement)target).IsEnabledCore); + Assert.False(((IInputElement)target).IsEffectivelyEnabled); } private class TestCommand : ICommand From 38d68865fde632c113833d8148f312300c4cc1c8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 17:14:44 +0200 Subject: [PATCH 003/179] Correctly handle command.CanExecute state. Added a new `IsEnabledCore` property to `InputElement` which is overridden in `Button` and `MenuItem` to override the `IsEffectivelyEnabled` state with the enabled state of the command. Also add data validation of the `Command` property to `MenuItem` to make it behave the same as `Button` when `Command` is bound to a non-existent property. Fixes #2501 --- src/Avalonia.Controls/Button.cs | 23 +++- src/Avalonia.Controls/MenuItem.cs | 36 ++++-- src/Avalonia.Input/InputElement.cs | 67 +++++------ .../ButtonTests.cs | 29 +++-- .../MenuItemTests.cs | 113 +++++++++++++++--- .../InputElement_Enabled.cs | 101 ++++++++++++++++ 6 files changed, 294 insertions(+), 75 deletions(-) create mode 100644 tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index cc9e6b7444..c47413c14b 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -33,8 +33,6 @@ namespace Avalonia.Controls /// public class Button : ContentControl { - private ICommand _command; - /// /// Defines the property. /// @@ -75,6 +73,9 @@ namespace Avalonia.Controls public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); + private ICommand _command; + private bool _commandCanExecute = true; + /// /// Initializes static members of the class. /// @@ -147,6 +148,8 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -289,7 +292,11 @@ namespace Avalonia.Controls { if (status?.ErrorType == BindingErrorType.Error) { - IsEnabled = false; + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } } } } @@ -348,9 +355,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d8473dc613..4cd215c238 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -20,8 +21,6 @@ namespace Avalonia.Controls /// public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable { - private ICommand _command; - /// /// Defines the property. /// @@ -91,9 +90,8 @@ namespace Avalonia.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); - /// - /// The submenu popup. - /// + private ICommand _command; + private bool _commandCanExecute = true; private Popup _popup; /// @@ -231,6 +229,8 @@ namespace Avalonia.Controls /// IMenuElement IMenuItem.Parent => Parent as IMenuElement; + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; + /// bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); @@ -400,6 +400,22 @@ namespace Avalonia.Controls } } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + { + base.UpdateDataValidation(property, status); + if (property == CommandProperty) + { + if (status?.ErrorType == BindingErrorType.Error) + { + if (_commandCanExecute) + { + _commandCanExecute = false; + UpdateIsEffectivelyEnabled(); + } + } + } + } + /// /// Closes all submenus of the menu item. /// @@ -443,9 +459,13 @@ namespace Avalonia.Controls /// The event args. private void CanExecuteChanged(object sender, EventArgs e) { - // HACK: Just set the IsEnabled property for the moment. This needs to be changed to - // use IsEnabledCore etc. but it will do for now. - IsEnabled = Command == null || Command.CanExecute(CommandParameter); + var canExecute = Command == null || Command.CanExecute(CommandParameter); + + if (canExecute != _commandCanExecute) + { + _commandCanExecute = canExecute; + UpdateIsEffectivelyEnabled(); + } } /// diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index a1b00f47fb..e1183e7154 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -29,8 +29,10 @@ namespace Avalonia.Input /// /// Defines the property. /// - public static readonly StyledProperty IsEffectivelyEnabledProperty = - AvaloniaProperty.Register(nameof(IsEffectivelyEnabled), true); + public static readonly DirectProperty IsEffectivelyEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsEffectivelyEnabled), + o => o.IsEffectivelyEnabled); /// /// Gets or sets associated mouse cursor. @@ -146,6 +148,7 @@ namespace Avalonia.Input /// public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + private bool _isEffectivelyEnabled = true; private bool _isFocused; private bool _isPointerOver; @@ -344,31 +347,25 @@ namespace Avalonia.Input internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } - /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. - /// - /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. - /// - bool IInputElement.IsEffectivelyEnabled => IsEffectivelyEnabled; + /// + public bool IsEffectivelyEnabled + { + get => _isEffectivelyEnabled; + private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value); + } + + public List KeyBindings { get; } = new List(); /// - /// Gets a value indicating whether the control is effectively enabled for user interaction. + /// Allows a derived class to override the enabled state of the control. /// /// - /// The property is used to toggle the enabled state for individual - /// controls. The property takes into account the - /// value of this control and its parent controls. + /// Derived controls may wish to disable the enabled state of the control without overwriting the + /// user-supplied setting. This can be done by overriding this property + /// to return the overridden enabled state. If the value returned from + /// should change, then the derived control should call . /// - protected bool IsEffectivelyEnabled - { - get { return GetValue(IsEffectivelyEnabledProperty); } - set { SetValue(IsEffectivelyEnabledProperty, value); } - } - - public List KeyBindings { get; } = new List(); + protected virtual bool IsEnabledCore => IsEnabled; /// /// Focuses the control. @@ -486,6 +483,15 @@ namespace Avalonia.Input { } + /// + /// Updates the property value according to the parent + /// control's enabled state and . + /// + protected void UpdateIsEffectivelyEnabled() + { + UpdateIsEffectivelyEnabled(this.GetVisualParent()); + } + private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e) { ((InputElement)e.Sender).UpdateIsEffectivelyEnabled(); @@ -511,14 +517,6 @@ namespace Avalonia.Input OnPointerLeave(e); } - /// - /// Updates the property value. - /// - private void UpdateIsEffectivelyEnabled() - { - UpdateIsEffectivelyEnabled(this.GetVisualParent()); - } - /// /// Updates the property based on the parent's /// . @@ -526,14 +524,7 @@ namespace Avalonia.Input /// The parent control. private void UpdateIsEffectivelyEnabled(InputElement parent) { - if (parent != null) - { - IsEffectivelyEnabled = IsEnabled && parent.IsEffectivelyEnabled; - } - else - { - IsEffectivelyEnabled = IsEnabled; - } + IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); foreach (var child in this.GetVisualChildren().OfType()) { diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9255b00e50..fe0ac47a7d 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -24,11 +24,11 @@ namespace Avalonia.Controls.UnitTests }; var root = new TestRoot { Child = target }; - Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); command.IsEnabled = true; - Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); command.IsEnabled = false; - Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -54,7 +54,8 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -72,8 +73,12 @@ namespace Avalonia.Controls.UnitTests }; Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + target.DataContext = null; - Assert.False(target.IsEnabled); + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] @@ -90,9 +95,13 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + target.DataContext = viewModel; + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); } [Fact] @@ -109,9 +118,13 @@ namespace Avalonia.Controls.UnitTests [!Button.CommandProperty] = new Binding("Command"), }; - Assert.False(target.IsEnabled); + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + target.DataContext = viewModel; - Assert.False(target.IsEnabled); + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index ebf2c72ab4..34371916df 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; +using Avalonia.Data; using Avalonia.Input; using Avalonia.UnitTests; using Xunit; @@ -26,6 +27,103 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.Focusable); } + + [Fact] + public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() + { + var command = new TestCommand(true); + var target = new MenuItem + { + IsEnabled = false, + Command = command, + }; + + var root = new TestRoot { Child = target }; + + Assert.False(((IInputElement)target).IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Bound_Command_Doesnt_Exist() + { + var target = new MenuItem + { + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Bound_Command_Is_Removed() + { + var viewModel = new + { + Command = new TestCommand(true), + }; + + var target = new MenuItem + { + DataContext = viewModel, + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + + target.DataContext = null; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Enabled_When_Bound_Command_Is_Added() + { + var viewModel = new + { + Command = new TestCommand(true), + }; + + var target = new MenuItem + { + DataContext = new object(), + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + + target.DataContext = viewModel; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + } + + [Fact] + public void MenuItem_Is_Disabled_When_Disabled_Bound_Command_Is_Added() + { + var viewModel = new + { + Command = new TestCommand(false), + }; + + var target = new MenuItem + { + DataContext = new object(), + [!MenuItem.CommandProperty] = new Binding("Command"), + }; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + + target.DataContext = viewModel; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + [Fact] public void MenuItem_Does_Not_Subscribe_To_Command_CanExecuteChanged_Until_Added_To_Logical_Tree() { @@ -59,21 +157,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(0, command.SubscriptionCount); } - [Fact] - public void MenuItem_Is_Disabled_When_Command_Is_Enabled_But_IsEnabled_Is_False() - { - var command = new TestCommand(true); - var target = new MenuItem - { - IsEnabled = false, - Command = command, - }; - - var root = new TestRoot { Child = target }; - - Assert.False(((IInputElement)target).IsEffectivelyEnabled); - } - private class TestCommand : ICommand { private bool _enabled; diff --git a/tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs b/tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs new file mode 100644 index 0000000000..5dd66e3190 --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/InputElement_Enabled.cs @@ -0,0 +1,101 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class InputElement_Enabled + { + [Fact] + public void IsEffectivelyEnabled_Follows_IsEnabled() + { + var target = new Decorator(); + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + + target.IsEnabled = false; + + Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + } + + [Fact] + public void IsEffectivelyEnabled_Follows_Ancestor_IsEnabled() + { + Decorator child; + Decorator grandchild; + var target = new Decorator + { + Child = child = new Decorator + { + Child = grandchild = new Decorator(), + } + }; + + Assert.True(target.IsEnabled); + Assert.True(target.IsEffectivelyEnabled); + Assert.True(child.IsEnabled); + Assert.True(child.IsEffectivelyEnabled); + Assert.True(grandchild.IsEnabled); + Assert.True(grandchild.IsEffectivelyEnabled); + + target.IsEnabled = false; + + Assert.False(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + Assert.True(child.IsEnabled); + Assert.False(child.IsEffectivelyEnabled); + Assert.True(grandchild.IsEnabled); + Assert.False(grandchild.IsEffectivelyEnabled); + } + + [Fact] + public void Disabled_Pseudoclass_Follows_IsEffectivelyEnabled() + { + Decorator child; + var target = new Decorator + { + Child = child = new Decorator() + }; + + Assert.DoesNotContain(":disabled", child.Classes); + + target.IsEnabled = false; + + Assert.Contains(":disabled", child.Classes); + } + + [Fact] + public void IsEffectivelyEnabled_Respects_IsEnabledCore() + { + Decorator child; + var target = new TestControl + { + Child = child = new Decorator() + }; + + target.ShouldEnable = false; + + Assert.True(target.IsEnabled); + Assert.False(target.IsEffectivelyEnabled); + Assert.True(child.IsEnabled); + Assert.False(child.IsEffectivelyEnabled); + } + + private class TestControl : Decorator + { + private bool _shouldEnable; + + public bool ShouldEnable + { + get => _shouldEnable; + set { _shouldEnable = value; UpdateIsEffectivelyEnabled(); } + } + + protected override bool IsEnabledCore => IsEnabled && _shouldEnable; + } + } +} From 5e5b090602bc748d0a84d0108d76268ccb34a1b0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 May 2019 17:24:31 +0200 Subject: [PATCH 004/179] Display a disabed menu item in ControlCatalog. --- samples/ControlCatalog/ViewModels/MenuPageViewModel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index 038f3574cc..88b1bf0b6b 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Reactive; +using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls; using ReactiveUI; @@ -11,7 +12,7 @@ namespace ControlCatalog.ViewModels public MenuPageViewModel() { OpenCommand = ReactiveCommand.CreateFromTask(Open); - SaveCommand = ReactiveCommand.Create(Save); + SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false)); OpenRecentCommand = ReactiveCommand.Create(OpenRecent); MenuItems = new[] From 22c5a5e6a1b0443488e5bddb037f1dece754e277 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 May 2019 17:43:43 +0200 Subject: [PATCH 005/179] Fix usage of AddClassHandler in non-static ctor. Property `AddClassHandler`s should only be added in static constructors. In this case it's not so important because `FocusManager` is a singleton, but best to fix the usage anyway. Fixes #2315. --- src/Avalonia.Input/FocusManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 102da6efc4..28fb26dca4 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -23,7 +23,7 @@ namespace Avalonia.Input /// /// Initializes a new instance of the class. /// - public FocusManager() + static FocusManager() { InputElement.PointerPressedEvent.AddClassHandler( typeof(IInputElement), @@ -174,7 +174,7 @@ namespace Avalonia.Input /// /// The event sender. /// The event args. - private void OnPreviewPointerPressed(object sender, RoutedEventArgs e) + private static void OnPreviewPointerPressed(object sender, RoutedEventArgs e) { var ev = (PointerPressedEventArgs)e; @@ -191,7 +191,7 @@ namespace Avalonia.Input if (element != null) { - Focus(element, NavigationMethod.Pointer, ev.InputModifiers); + Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers); } } } From fb08f031094c8904855c1b474bf64a2dd9c3ac34 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 May 2019 17:44:26 +0200 Subject: [PATCH 006/179] Add a note to fix usage of AddClassHandler. I plan on replacing DevTools with something newer soon, so just add a FIXME comment for now. --- src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs index 59c0f82414..7ece790310 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs @@ -56,6 +56,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (IsEnabled.GetValueOrDefault() && !_isRegistered) { + // FIXME: This leaks event handlers. _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true); _isRegistered = true; } From 7cdaeac4411bb95d56b7602c3c99a38de17b6437 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 May 2019 18:41:58 +0200 Subject: [PATCH 007/179] Make Rect.Deflate consistent with Rect.Inflate. `Rect.Deflate` was halving the thickness passed into it whereas `Rect.Inflate` wasn't. Make both methods consistent and _not_ halve the thickness. --- src/Avalonia.Controls/Shapes/Ellipse.cs | 2 +- src/Avalonia.Controls/Shapes/Rectangle.cs | 2 +- src/Avalonia.Visuals/Rect.cs | 12 +++++------- .../Controls/CustomRenderTests.cs | 6 +++--- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Ellipse.cs b/src/Avalonia.Controls/Shapes/Ellipse.cs index 8cb73bf5c6..f80bf5d967 100644 --- a/src/Avalonia.Controls/Shapes/Ellipse.cs +++ b/src/Avalonia.Controls/Shapes/Ellipse.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Shapes protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness); + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); return new EllipseGeometry(rect); } diff --git a/src/Avalonia.Controls/Shapes/Rectangle.cs b/src/Avalonia.Controls/Shapes/Rectangle.cs index 375b0b719a..b803bde588 100644 --- a/src/Avalonia.Controls/Shapes/Rectangle.cs +++ b/src/Avalonia.Controls/Shapes/Rectangle.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Shapes protected override Geometry CreateDefiningGeometry() { - var rect = new Rect(Bounds.Size).Deflate(StrokeThickness); + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness / 2); return new RectangleGeometry(rect); } diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 530a47729f..49d4724b9a 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -256,7 +256,7 @@ namespace Avalonia /// /// Inflates the rectangle. /// - /// The thickness. + /// The thickness to be subtracted for each side of the rectangle. /// The inflated rectangle. public Rect Inflate(double thickness) { @@ -266,7 +266,7 @@ namespace Avalonia /// /// Inflates the rectangle. /// - /// The thickness. + /// The thickness to be subtracted for each side of the rectangle. /// The inflated rectangle. public Rect Inflate(Thickness thickness) { @@ -278,20 +278,18 @@ namespace Avalonia /// /// Deflates the rectangle. /// - /// The thickness. + /// The thickness to be subtracted for each side of the rectangle. /// The deflated rectangle. - /// The deflated rectangle size cannot be less than 0. public Rect Deflate(double thickness) { - return Deflate(new Thickness(thickness / 2)); + return Deflate(new Thickness(thickness)); } /// /// Deflates the rectangle by a . /// - /// The thickness. + /// The thickness to be subtracted for each side of the rectangle. /// The deflated rectangle. - /// The deflated rectangle size cannot be less than 0. public Rect Deflate(Thickness thickness) { return new Rect( diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs index 6a01536b12..d3a935c281 100644 --- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls new Rect(control.Bounds.Size), 4); - using (context.PushClip(new Rect(control.Bounds.Size).Deflate(20))) + using (context.PushClip(new Rect(control.Bounds.Size).Deflate(10))) { context.FillRectangle( Brushes.Blue, @@ -100,7 +100,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { context.FillRectangle( Brushes.Blue, - new Rect(control.Bounds.Size).Deflate(20), + new Rect(control.Bounds.Size).Deflate(10), 4); } }), @@ -140,7 +140,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { context.FillRectangle( Brushes.Blue, - new Rect(control.Bounds.Size).Deflate(20), + new Rect(control.Bounds.Size).Deflate(10), 4); } }), From 88f2bd791d17ff67b6d369a4b19d9d836fb6856e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 May 2019 18:58:07 +0200 Subject: [PATCH 008/179] Fix hit testing in RectangleNode. There were two problems here: - As #2370 describes we were tested against the rotated AABB of the rectangle - We were hit-testing non-filled rectangles as filled rectangles Fixes #2370 --- .../Rendering/SceneGraph/RectangleNode.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 33cc39cbe3..c622dc8a43 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -105,8 +105,28 @@ namespace Avalonia.Rendering.SceneGraph } } - // TODO: This doesn't respect CornerRadius yet. /// - public override bool HitTest(Point p) => Bounds.Contains(p); + public override bool HitTest(Point p) + { + // TODO: This doesn't respect CornerRadius yet. + if (Transform.HasInverse) + { + p *= Transform.Invert(); + + if (Brush != null) + { + var rect = Rect.Inflate((Pen?.Thickness / 2) ?? 0); + return rect.Contains(p); + } + else + { + var borderRect = Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Deflate((Pen?.Thickness / 2) ?? 0); + return borderRect.Contains(p) && !emptyRect.Contains(p); + } + } + + return false; + } } } From 2fcad75a6bf1385aaa5cf0b47459d769dff58b49 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 May 2019 08:59:18 +0200 Subject: [PATCH 009/179] Added failing test for #2535. --- .../VisualExtensionsTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs diff --git a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs new file mode 100644 index 0000000000..a8d8c07d8b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs @@ -0,0 +1,38 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Visuals.UnitTests +{ + public class VisualExtensionsTests + { + [Fact] + public void TranslatePoint_Should_Respect_RenderTransforms() + { + Border target; + var root = new TestRoot + { + Width = 100, + Height = 100, + Child = new Decorator + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + RenderTransform = new TranslateTransform(25, 25), + Child = target = new Border(), + } + }; + + root.Measure(Size.Infinity); + root.Arrange(new Rect(root.DesiredSize)); + + var result = target.TranslatePoint(new Point(0, 0), root); + + Assert.Equal(new Point(50, 50), result); + } + } +} From 9eac3b6203e5115a6b69ff02f7ee6a98e4fbf7bb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 May 2019 09:05:37 +0200 Subject: [PATCH 010/179] Fix TranslatePoint. - Make `VisualExtensions.TranslatePoint` respect render transforms. - Move `IVisual.TransformToVisual` out of the interface and into `VisualExtensions` as an extension method - Make `TranslatePoint` return a nullable value as `TransformToVisual` does, so it can return null when controls don't have a common ancestor --- src/Avalonia.Input/DragDropDevice.cs | 10 +- src/Avalonia.Input/DragEventArgs.cs | 5 +- .../Rendering/SceneGraph/Scene.cs | 2 +- src/Avalonia.Visuals/Visual.cs | 62 ------------- src/Avalonia.Visuals/VisualExtensions.cs | 92 ++++++++++++++----- src/Avalonia.Visuals/VisualTree/IVisual.cs | 11 --- .../VisualTree/VisualExtensions.cs | 10 +- 7 files changed, 87 insertions(+), 105 deletions(-) diff --git a/src/Avalonia.Input/DragDropDevice.cs b/src/Avalonia.Input/DragDropDevice.cs index 0692b21c66..0b9f09b224 100644 --- a/src/Avalonia.Input/DragDropDevice.cs +++ b/src/Avalonia.Input/DragDropDevice.cs @@ -23,7 +23,13 @@ namespace Avalonia.Input { if (target == null) return DragDropEffects.None; - var args = new DragEventArgs(routedEvent, data, target, inputRoot.TranslatePoint(point, target), modifiers) + + var p = inputRoot.TranslatePoint(point, target); + + if (!p.HasValue) + return DragDropEffects.None; + + var args = new DragEventArgs(routedEvent, data, target, p.Value, modifiers) { RoutedEvent = routedEvent, DragEffects = operation @@ -108,4 +114,4 @@ namespace Avalonia.Input } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/DragEventArgs.cs b/src/Avalonia.Input/DragEventArgs.cs index 915ee4ee5c..dc0b76b225 100644 --- a/src/Avalonia.Input/DragEventArgs.cs +++ b/src/Avalonia.Input/DragEventArgs.cs @@ -26,8 +26,9 @@ namespace Avalonia.Input if (_target != null) { - point = _target.TranslatePoint(_targetLocation, relativeTo); + point = _target.TranslatePoint(_targetLocation, relativeTo) ?? point; } + return point; } @@ -41,4 +42,4 @@ namespace Avalonia.Input } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index ad4c475d89..1afc096c98 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -173,7 +173,7 @@ namespace Avalonia.Rendering.SceneGraph if (node.GeometryClip != null) { var controlPoint = Root.Visual.TranslatePoint(p, node.Visual); - clipped = !node.GeometryClip.FillContains(controlPoint); + clipped = !node.GeometryClip.FillContains(controlPoint.Value); } if (!clipped) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index d294f111ad..9e088cb136 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -291,29 +291,6 @@ namespace Avalonia Contract.Requires(context != null); } - /// - /// Returns a transform that transforms the visual's coordinates into the coordinates - /// of the specified . - /// - /// The visual to translate the coordinates to. - /// - /// A containing the transform or null if the visuals don't share a - /// common ancestor. - /// - public Matrix? TransformToVisual(IVisual visual) - { - var common = this.FindCommonVisualAncestor(visual); - - if (common != null) - { - var thisOffset = GetOffsetFrom(common, this); - var thatOffset = GetOffsetFrom(common, visual); - return -thatOffset * thisOffset; - } - - return null; - } - /// /// Indicates that a property change should cause to be /// called. @@ -480,45 +457,6 @@ namespace Avalonia } } - /// - /// Gets the visual offset from the specified ancestor. - /// - /// The ancestor visual. - /// The visual. - /// The visual offset. - private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual) - { - var result = Matrix.Identity; - - while (visual != ancestor) - { - if (visual.RenderTransform?.Value != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(visual.Bounds.Size); - var offset = Matrix.CreateTranslation(origin); - var renderTransform = (-offset) * visual.RenderTransform.Value * (offset); - - result *= renderTransform; - } - - var topLeft = visual.Bounds.TopLeft; - - if (topLeft != default) - { - result *= Matrix.CreateTranslation(topLeft); - } - - visual = visual.VisualParent; - - if (visual == null) - { - throw new ArgumentException("'visual' is not a descendant of 'ancestor'."); - } - } - - return result; - } - /// /// Called when a visual's changes. /// diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs index 650921a985..b191d044db 100644 --- a/src/Avalonia.Visuals/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.Rendering; using Avalonia.VisualTree; namespace Avalonia @@ -20,10 +19,8 @@ namespace Avalonia /// The point in client coordinates. public static Point PointToClient(this IVisual visual, PixelPoint point) { - var (root, offset) = GetRootAndPosition(visual); - var screenOffset = PixelPoint.FromPoint((Point)offset, root.RenderScaling); - var screenPoint = new PixelPoint(point.X - screenOffset.X, point.Y - screenOffset.Y); - return root.PointToClient(screenPoint); + var rootPoint = visual.VisualRoot.PointToClient(point); + return visual.VisualRoot.TranslatePoint(rootPoint, visual).Value; } /// @@ -34,48 +31,93 @@ namespace Avalonia /// The point in screen coordinates. public static PixelPoint PointToScreen(this IVisual visual, Point point) { - var p = GetRootAndPosition(visual); - return p.Item1.PointToScreen(point + p.Item2); + var p = visual.TranslatePoint(point, visual.VisualRoot); + return visual.VisualRoot.PointToScreen(p.Value); + } + + /// + /// Returns a transform that transforms the visual's coordinates into the coordinates + /// of the specified . + /// + /// The visual whose coordinates are to be transformed. + /// The visual to translate the coordinates to. + /// + /// A containing the transform or null if the visuals don't share a + /// common ancestor. + /// + public static Matrix? TransformToVisual(this IVisual from, IVisual to) + { + var common = from.FindCommonVisualAncestor(to); + + if (common != null) + { + var thisOffset = GetOffsetFrom(common, from); + var thatOffset = GetOffsetFrom(common, to); + return -thatOffset * thisOffset; + } + + return null; } /// /// Translates a point relative to this visual to coordinates that are relative to the specified visual. - /// The visual and relativeTo should be descendants of the same root window /// /// The visual. /// The point value, as relative to this visual. /// The visual to translate the given point into. - /// A point value, now relative to the target visual rather than this source element. - public static Point TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) + /// + /// A point value, now relative to the target visual rather than this source element, or null if the + /// two elements have no common ancestor. + /// + public static Point? TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) { - var pos = GetRootAndPosition(visual); - var relToPos = GetRootAndPosition(relativeTo); + var transform = visual.TransformToVisual(relativeTo); - return point - (relToPos.Item2 - pos.Item2); + if (transform.HasValue) + { + return point.Transform(transform.Value); + } + + return null; } /// - /// Gets the root of the control's visual tree and the position of the control - /// in the root's coordinate space. + /// Gets a transform from an ancestor to a descendent. /// - /// The visual. - /// A tuple containing the root and the position of the control. - private static Tuple GetRootAndPosition(IVisual v) + /// The ancestor visual. + /// The visual. + /// The transform. + private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual) { - var result = new Vector(); + var result = Matrix.Identity; - while (!(v is IRenderRoot)) + while (visual != ancestor) { - result = new Vector(result.X + v.Bounds.X, result.Y + v.Bounds.Y); - v = v.VisualParent; + if (visual.RenderTransform?.Value != null) + { + var origin = visual.RenderTransformOrigin.ToPixels(visual.Bounds.Size); + var offset = Matrix.CreateTranslation(origin); + var renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + + result *= renderTransform; + } + + var topLeft = visual.Bounds.TopLeft; + + if (topLeft != default) + { + result *= Matrix.CreateTranslation(topLeft); + } + + visual = visual.VisualParent; - if (v == null) + if (visual == null) { - throw new InvalidOperationException("Control is not attached to visual tree."); + throw new ArgumentException("'visual' is not a descendant of 'ancestor'."); } } - return Tuple.Create((IRenderRoot)v, result); + return result; } } } diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs index 278a802597..dd3686ce3d 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisual.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs @@ -116,16 +116,5 @@ namespace Avalonia.VisualTree /// /// The context. void Render(DrawingContext context); - - /// - /// Returns a transform that transforms the visual's coordinates into the coordinates - /// of the specified . - /// - /// The visual to translate the coordinates to. - /// - /// A containing the transform or null if the visuals don't share a - /// common ancestor. - /// - Matrix? TransformToVisual(IVisual visual); } } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 2d417852fe..567b676b1e 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -132,8 +132,14 @@ namespace Avalonia.VisualTree Contract.Requires(visual != null); var root = visual.GetVisualRoot(); - p = visual.TranslatePoint(p, root); - return root.Renderer.HitTest(p, visual, filter); + var rootPoint = visual.TranslatePoint(p, root); + + if (rootPoint.HasValue) + { + return root.Renderer.HitTest(rootPoint.Value, visual, filter); + } + + return Enumerable.Empty(); } /// From 8cfa7c175c4e9094d52448137db7731532a50c26 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Fri, 17 May 2019 15:58:36 +0200 Subject: [PATCH 011/179] Add blank cursor --- src/Avalonia.Input/Cursors.cs | 1 + src/Avalonia.X11/X11CursorFactory.cs | 22 ++++++++++++++++++++- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 7 ++++--- src/Gtk/Avalonia.Gtk3/GdkCursor.cs | 1 + src/Windows/Avalonia.Win32/CursorFactory.cs | 5 +++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 40b01117e3..c5566318a2 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -8,6 +8,8 @@ namespace Avalonia.X11 { class X11CursorFactory : IStandardCursorFactory { + private static IntPtr _nullCursor; + private readonly IntPtr _display; private Dictionary _cursors; @@ -42,16 +44,34 @@ namespace Avalonia.X11 public X11CursorFactory(IntPtr display) { _display = display; + _nullCursor = GetNullCursor(display); _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } public IPlatformHandle GetCursor(StandardCursorType cursorType) { - var handle = s_mapping.TryGetValue(cursorType, out var shape) + IntPtr handle; + if (cursorType == StandardCursorType.None) + { + handle = _nullCursor; + } + else + { + handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; + } return new PlatformHandle(handle, "XCURSOR"); } + + private static IntPtr GetNullCursor(IntPtr display) + { + XColor color = new XColor(); + byte[] data = new byte[] { 0 }; + IntPtr window = XLib.XRootWindow(display, 0); + IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index a28b1cbb1a..f36e6b986d 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -12,7 +12,8 @@ namespace Avalonia.Gtk3 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.None, CursorType.Blank}, + { StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -36,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = @@ -80,4 +81,4 @@ namespace Avalonia.Gtk3 return rv; } } -} \ No newline at end of file +} diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs index 4fad8208b3..aa0f8cde0d 100644 --- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs +++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs @@ -2,6 +2,7 @@ { enum GdkCursorType { + Blank = -2, CursorIsPixmap = -1, XCursor = 0, Arrow = 2, diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index e582b5fb82..df413addef 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -41,7 +41,8 @@ namespace Avalonia.Win32 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, 32650}, + {StandardCursorType.None, 0}, + { StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -69,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From 4ad1acd24eeb836ff3b4fb06cb83011ed635d1e0 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:05:49 +0200 Subject: [PATCH 012/179] Fix X11 NullCursor --- src/Avalonia.X11/X11CursorFactory.cs | 8 ++++---- src/Avalonia.X11/XLib.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c5566318a2..c020c44662 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -51,7 +51,7 @@ namespace Avalonia.X11 public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -66,12 +66,12 @@ namespace Avalonia.X11 } private static IntPtr GetNullCursor(IntPtr display) - { + { XColor color = new XColor(); byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); - IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a146f922d..3c41f7bdde 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -321,6 +321,9 @@ namespace Avalonia.X11 public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask, ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot); + [DllImport(libX11)] + public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height); + [DllImport(libX11)] public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width, int height, IntPtr fg, IntPtr bg, int depth); From 3860fafa000e9127f436745b998f6d8994cdb715 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:10:49 +0200 Subject: [PATCH 013/179] Fix formatting --- src/Avalonia.X11/X11CursorFactory.cs | 10 +++++----- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 4 ++-- src/Windows/Avalonia.Win32/CursorFactory.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c020c44662..0a8b1ee9c4 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11 class X11CursorFactory : IStandardCursorFactory { private static IntPtr _nullCursor; - + private readonly IntPtr _display; private Dictionary _cursors; @@ -48,10 +48,10 @@ namespace Avalonia.X11 _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -61,7 +61,7 @@ namespace Avalonia.X11 handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; - } + } return new PlatformHandle(handle, "XCURSOR"); } @@ -71,7 +71,7 @@ namespace Avalonia.X11 byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index f36e6b986d..95fa3ba9e3 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -13,7 +13,7 @@ namespace Avalonia.Gtk3 { {StandardCursorType.None, CursorType.Blank}, - { StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -37,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index df413addef..f1fd74f931 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -42,7 +42,7 @@ namespace Avalonia.Win32 { {StandardCursorType.None, 0}, - { StandardCursorType.AppStarting, 32650}, + {StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -70,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From c72195f45b3cb68ac259dba472889827973295ca Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 20 May 2019 15:15:30 +0100 Subject: [PATCH 014/179] Fix use of CoerceCaretIndex on Text change --- src/Avalonia.Controls/TextBox.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 04b088e35c..d43957313e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -214,9 +214,9 @@ namespace Avalonia.Controls if (!_ignoreTextChanges) { var caretIndex = CaretIndex; - SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0); - SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0); - CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0); + SelectionStart = CoerceCaretIndex(SelectionStart, value); + SelectionEnd = CoerceCaretIndex(SelectionEnd, value); + CaretIndex = CoerceCaretIndex(caretIndex, value); if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing) { @@ -677,11 +677,15 @@ namespace Avalonia.Controls } } - private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0); + private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text); - private int CoerceCaretIndex(int value, int length) + private int CoerceCaretIndex(int value, string text) { - var text = Text; + if (text == null) + { + return 0; + } + var length = text.Length; if (value < 0) { @@ -691,7 +695,7 @@ namespace Avalonia.Controls { return length; } - else if (value > 0 && text[value - 1] == '\r' && text[value] == '\n') + else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n') { return value + 1; } From 8a9e997c6c9b4302c22ebee4cfe729453e83e231 Mon Sep 17 00:00:00 2001 From: ahopper Date: Tue, 21 May 2019 07:13:26 +0100 Subject: [PATCH 015/179] unit test added --- tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0d87f6d0fe..9b62509138 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -385,6 +385,21 @@ namespace Avalonia.Controls.UnitTests Assert.True(target.SelectionEnd <= "123".Length); } } + [Fact] + public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123456789\r" + }; + target.CaretIndex = 11; + + Assert.True(true); + } + } private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); From c3f142af2e75c83c57262e9ab27efa7c25e02043 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 18:35:47 +0300 Subject: [PATCH 016/179] Add ViewModelViewHost and TransitioningUserControl --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 143 +++--------------- .../TransitioningUserControl.cs | 127 ++++++++++++++++ src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 75 +++++++++ 3 files changed, 219 insertions(+), 126 deletions(-) create mode 100644 src/Avalonia.ReactiveUI/TransitioningUserControl.cs create mode 100644 src/Avalonia.ReactiveUI/ViewModelViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..bf295aafca 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningUserControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, value); } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs new file mode 100644 index 0000000000..fb5258f2e4 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs @@ -0,0 +1,127 @@ +// 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 Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningUserControl : UserControl + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when content disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the content with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); + base.Content = content; + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..84108a9b52 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,75 @@ +// 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 ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningUserControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Updates the Content when ViewModel changes. + /// + /// Property changed event arguments. + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); + base.OnPropertyChanged(e); + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file From 97b44c02d25d07c0e2adfecb842e016c54d2e434 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:09:51 +0300 Subject: [PATCH 017/179] Add a unit test for ViewModelViewHost --- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 27 ++++--- .../Attributes.cs | 6 ++ .../AvaloniaActivationForViewFetcherTest.cs | 2 +- .../RoutedViewHostTest.cs | 2 +- .../ViewModelViewHostTest.cs | 72 +++++++++++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 84108a9b52..6f80bff4b8 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.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.Reactive.Disposables; using ReactiveUI; using Splat; @@ -18,7 +20,20 @@ namespace Avalonia.ReactiveUI /// public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); - + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + /// /// Gets or sets the ViewModel to display. /// @@ -33,16 +48,6 @@ namespace Avalonia.ReactiveUI /// public IViewLocator ViewLocator { get; set; } - /// - /// Updates the Content when ViewModel changes. - /// - /// Property changed event arguments. - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); - base.OnPropertyChanged(e); - } - /// /// Invoked when ReactiveUI router navigates to a view model. /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs new file mode 100644 index 0000000000..79e58f63e9 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -0,0 +1,6 @@ +using Xunit; + +// Required to avoid InvalidOperationException sometimes thrown +// from Splat.MemoizingMRUCache.cs which is not thread-safe. +// Thrown when trying to access WhenActivated concurrently. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index d9f1ce47dd..f4dffdc0c3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -13,7 +13,7 @@ using Splat; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class AvaloniaActivationForViewFetcherTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index 401d169896..c6017d3f5f 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; using System.Reactive; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class RoutedViewHostTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs new file mode 100644 index 0000000000..5d5d15358e --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ViewModelViewHostTest + { + public class FirstViewModel { } + + public class FirstView : ReactiveUserControl { } + + public class SecondViewModel : ReactiveObject { } + + public class SecondView : ReactiveUserControl { } + + public ViewModelViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor)); + } + + [Fact] + public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel() + { + var defaultContent = new TextBlock(); + var host = new ViewModelViewHost + { + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstViewModel(); + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + + var second = new SecondViewModel(); + host.ViewModel = second; + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondView), host.Content.GetType()); + Assert.Equal(second, ((SecondView)host.Content).DataContext); + Assert.Equal(second, ((SecondView)host.Content).ViewModel); + + host.ViewModel = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + } + } +} \ No newline at end of file From 724a5da8963ea83d616fce8691a9ded211ccb4ef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:10:40 +0300 Subject: [PATCH 018/179] Add license headers --- tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs | 3 +++ .../AvaloniaActivationForViewFetcherTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs index 79e58f63e9..a79647d355 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Xunit; // Required to avoid InvalidOperationException sometimes thrown diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index f4dffdc0c3..2c81e8fea3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index c6017d3f5f..e873c60e36 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 5d5d15358e..a21bf34ef5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Controls; using Avalonia.UnitTests; using ReactiveUI; From e8c0012c318522529f0e572a7535ab60d628d96e Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:26:29 +0300 Subject: [PATCH 019/179] Unit tests for ReactiveWindow and ReactiveUserControl --- .../ReactiveUserControlTest.cs | 34 +++++++++++++++++ .../ReactiveWindowTest.cs | 37 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs new file mode 100644 index 0000000000..328b749ba1 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveUserControlTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleView : ReactiveUserControl { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() + { + var view = new ExampleView(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs new file mode 100644 index 0000000000..ff77de8d28 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveWindowTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleWindow : ReactiveWindow { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var view = new ExampleWindow(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } + } +} \ No newline at end of file From fe7e7054fb7012b8f901de4d7abc8664bc9f40e9 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 20:40:26 +0300 Subject: [PATCH 020/179] Add initial AutoDataTemplateBindingHook implementation --- .../AutoDataTemplateBindingHook.cs | 60 +++++++++++++++++++ .../AutoDataTemplateBindingHookTest.cs | 56 +++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..bd156a5884 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace Avalonia.ReactiveUI +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + { + var template = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + return (DataTemplate)loader.Load(template); + }); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + if (lastViewProperty == null) + return true; + + var itemsControl = lastViewProperty.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate.Value; + return true; + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs new file mode 100644 index 0000000000..2391daece2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -0,0 +1,56 @@ +// 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 Xunit; +using ReactiveUI; +using Avalonia.ReactiveUI; +using Avalonia.UnitTests; +using Avalonia.Controls; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Splat; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class AutoDataTemplateBindingHookTest + { + public class NestedViewModel : ReactiveObject { } + + public class NestedView : ReactiveUserControl { } + + public class ExampleViewModel : ReactiveObject + { + public ObservableCollection Items { get; } = new ObservableCollection(); + } + + public class ExampleView : ReactiveUserControl + { + public ListBox List { get; } = new ListBox(); + + public ExampleView() + { + Content = List; + ViewModel = new ExampleViewModel(); + this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items); + } + } + + public AutoDataTemplateBindingHookTest() + { + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + } + + [Fact] + public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() + { + var view = new ExampleView(); + var root = new TestRoot + { + Child = view + }; + Assert.NotNull(view.List.ItemTemplate); + } + } +} \ No newline at end of file From b486112d296354f0d9d46f4fea72ea8bdb5c2bef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:06:31 +0300 Subject: [PATCH 021/179] Use null propagation --- src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bd156a5884..bb660fd76a 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -38,10 +38,7 @@ namespace Avalonia.ReactiveUI { var viewProperties = getCurrentViewProperties(); var lastViewProperty = viewProperties.LastOrDefault(); - if (lastViewProperty == null) - return true; - - var itemsControl = lastViewProperty.Sender as ItemsControl; + var itemsControl = lastViewProperty?.Sender as ItemsControl; if (itemsControl == null) return true; From 72fe95272a09644f72fd34c7d14acf42ef576819 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:08:52 +0300 Subject: [PATCH 022/179] Register auto data template binding hook --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index f67cb7f40a..ced26a3004 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.Register( - () => new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); }); } } From d2574b383a1880bdd3aebfc94b9ee26ce653629d Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:47:18 +0300 Subject: [PATCH 023/179] Use FuncDataTemplate --- .../AutoDataTemplateBindingHook.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bb660fd76a..d7cfebf1d8 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Templates; using ReactiveUI; @@ -14,19 +16,18 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => { - var template = @" - - -"; - - var loader = new AvaloniaXamlLoader(); - return (DataTemplate)loader.Load(template); + return new FuncDataTemplate(x => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); }); /// From 65f5f58a07f29c5baf4fe80218cc2a83deba5dc6 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 00:36:53 +0300 Subject: [PATCH 024/179] ViewModelViewHost type resolution assertion --- .../AutoDataTemplateBindingHookTest.cs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 2391daece2..4ff3e1fed6 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -6,9 +6,12 @@ using ReactiveUI; using Avalonia.ReactiveUI; using Avalonia.UnitTests; using Avalonia.Controls; +using Avalonia.Controls.Templates; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.VisualTree; +using Avalonia.Controls.Presenters; using Splat; namespace Avalonia.ReactiveUI.UnitTests @@ -26,7 +29,7 @@ namespace Avalonia.ReactiveUI.UnitTests public class ExampleView : ReactiveUserControl { - public ListBox List { get; } = new ListBox(); + public ItemsControl List { get; } = new ItemsControl(); public ExampleView() { @@ -40,17 +43,48 @@ namespace Avalonia.ReactiveUI.UnitTests { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); } [Fact] public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() { var view = new ExampleView(); - var root = new TestRoot - { - Child = view - }; Assert.NotNull(view.List.ItemTemplate); } + + [Fact] + public void Should_Use_View_Model_View_Host_As_Data_Template() + { + var view = new ExampleView(); + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + Assert.IsType(container.Child); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + MemberSelector = parent.MemberSelector, + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + } + }; + }); + } } } \ No newline at end of file From 8a2d1662fdcd92057d39aa39a2e8296e134ab0bd Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 01:20:36 +0300 Subject: [PATCH 025/179] Remove LazyT, test new template resolver --- .../AutoDataTemplateBindingHook.cs | 23 +++++++-------- .../AutoDataTemplateBindingHookTest.cs | 28 ++++++++++++++++++- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index d7cfebf1d8..3f41f54363 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -16,19 +16,16 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => { - return new FuncDataTemplate(x => - { - var control = new ViewModelViewHost(); - var context = control.GetObservable(Control.DataContextProperty); - control.Bind(ViewModelViewHost.ViewModelProperty, context); - control.HorizontalContentAlignment = HorizontalAlignment.Stretch; - control.VerticalContentAlignment = VerticalAlignment.Stretch; - return control; - }, - true); - }); + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); /// public bool ExecuteHook( @@ -51,7 +48,7 @@ namespace Avalonia.ReactiveUI if (itemsControl.ItemTemplate != null) return true; - itemsControl.ItemTemplate = DefaultItemTemplate.Value; + itemsControl.ItemTemplate = DefaultItemTemplate; return true; } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 4ff3e1fed6..667462eb91 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -13,6 +13,8 @@ using System.Linq; using Avalonia.VisualTree; using Avalonia.Controls.Presenters; using Splat; +using System.Threading.Tasks; +using System; namespace Avalonia.ReactiveUI.UnitTests { @@ -42,8 +44,8 @@ namespace Avalonia.ReactiveUI.UnitTests public AutoDataTemplateBindingHookTest() { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor)); } [Fact] @@ -70,6 +72,30 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.IsType(container.Child); } + [Fact] + public void Should_Resolve_And_Embedd_Appropriate_View_Model() + { + var view = new ExampleView(); + var root = new TestRoot { Child = view }; + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + var host = (ViewModelViewHost) container.Child; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + + host.DataContext = "changed context"; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate(parent => From c4fccd5dd7b606a4b5ae1bf35a3bb70027794320 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 23 May 2019 12:34:38 +0800 Subject: [PATCH 026/179] Import Grid code from WPF.Core --- src/Avalonia.Controls/GridWPF.cs | 4420 ++++++++++++++++++++++++++++++ 1 file changed, 4420 insertions(+) create mode 100644 src/Avalonia.Controls/GridWPF.cs diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs new file mode 100644 index 0000000000..3a4b41314f --- /dev/null +++ b/src/Avalonia.Controls/GridWPF.cs @@ -0,0 +1,4420 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Collections; +using Avalonia.Controls.Utils; +using Avalonia.VisualTree; +using JetBrains.Annotations; + + +using MS.Internal; +using MS.Internal.Controls; +using MS.Internal.PresentationFramework; +using MS.Internal.Telemetry.PresentationFramework; +using MS.Utility; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Windows.Threading; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Markup; + +#pragma warning disable 1634, 1691 // suppressing PreSharp warnings + +namespace System.Windows.Controls +{ + /// + /// Grid + /// + public class Grid : Panel, IAddChild + { + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + #region Constructors + + static Grid() + { + ControlsTraceLogger.AddControl(TelemetryControls.Grid); + } + + /// + /// Default constructor. + /// + public Grid() + { + SetFlags((bool) ShowGridLinesProperty.GetDefaultValue(DependencyObjectType), Flags.ShowGridLinesPropertyValue); + } + + #endregion Constructors + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + /// + /// + /// + void IAddChild.AddChild(object value) + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + UIElement cell = value as UIElement; + if (cell != null) + { + Children.Add(cell); + return; + } + + throw (new ArgumentException(SR.Get(SRID.Grid_UnexpectedParameterType, value.GetType(), typeof(UIElement)), "value")); + } + + /// + /// + /// + void IAddChild.AddText(string text) + { + XamlSerializerUtil.ThrowIfNonWhiteSpaceInAddText(text, this); + } + + /// + /// + /// + protected internal override IEnumerator LogicalChildren + { + get + { + // empty panel or a panel being used as the items + // host has *no* logical children; give empty enumerator + bool noChildren = (base.VisualChildrenCount == 0) || IsItemsHost; + + if (noChildren) + { + ExtendedData extData = ExtData; + + if ( extData == null + || ( (extData.ColumnDefinitions == null || extData.ColumnDefinitions.Count == 0) + && (extData.RowDefinitions == null || extData.RowDefinitions.Count == 0) ) + ) + { + // grid is empty + return EmptyEnumerator.Instance; + } + } + + return (new GridChildrenCollectionEnumeratorSimple(this, !noChildren)); + } + } + + /// + /// Helper for setting Column property on a UIElement. + /// + /// UIElement to set Column property on. + /// Column property value. + public static void SetColumn(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(ColumnProperty, value); + } + + /// + /// Helper for reading Column property from a UIElement. + /// + /// UIElement to read Column property from. + /// Column property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetColumn(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(ColumnProperty)); + } + + /// + /// Helper for setting Row property on a UIElement. + /// + /// UIElement to set Row property on. + /// Row property value. + public static void SetRow(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(RowProperty, value); + } + + /// + /// Helper for reading Row property from a UIElement. + /// + /// UIElement to read Row property from. + /// Row property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetRow(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(RowProperty)); + } + + /// + /// Helper for setting ColumnSpan property on a UIElement. + /// + /// UIElement to set ColumnSpan property on. + /// ColumnSpan property value. + public static void SetColumnSpan(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(ColumnSpanProperty, value); + } + + /// + /// Helper for reading ColumnSpan property from a UIElement. + /// + /// UIElement to read ColumnSpan property from. + /// ColumnSpan property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetColumnSpan(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(ColumnSpanProperty)); + } + + /// + /// Helper for setting RowSpan property on a UIElement. + /// + /// UIElement to set RowSpan property on. + /// RowSpan property value. + public static void SetRowSpan(UIElement element, int value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(RowSpanProperty, value); + } + + /// + /// Helper for reading RowSpan property from a UIElement. + /// + /// UIElement to read RowSpan property from. + /// RowSpan property value. + [AttachedPropertyBrowsableForChildren()] + public static int GetRowSpan(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((int)element.GetValue(RowSpanProperty)); + } + + /// + /// Helper for setting IsSharedSizeScope property on a UIElement. + /// + /// UIElement to set IsSharedSizeScope property on. + /// IsSharedSizeScope property value. + public static void SetIsSharedSizeScope(UIElement element, bool value) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + element.SetValue(IsSharedSizeScopeProperty, value); + } + + /// + /// Helper for reading IsSharedSizeScope property from a UIElement. + /// + /// UIElement to read IsSharedSizeScope property from. + /// IsSharedSizeScope property value. + public static bool GetIsSharedSizeScope(UIElement element) + { + if (element == null) + { + throw new ArgumentNullException("element"); + } + + return ((bool)element.GetValue(IsSharedSizeScopeProperty)); + } + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + #region Public Properties + + /// + /// ShowGridLines property. + /// + public bool ShowGridLines + { + get { return (CheckFlagsAnd(Flags.ShowGridLinesPropertyValue)); } + set { SetValue(ShowGridLinesProperty, value); } + } + + /// + /// Returns a ColumnDefinitionCollection of column definitions. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public ColumnDefinitionCollection ColumnDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitionCollection(this); } + + return (_data.ColumnDefinitions); + } + } + + /// + /// Returns a RowDefinitionCollection of row definitions. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] + public RowDefinitionCollection RowDefinitions + { + get + { + if (_data == null) { _data = new ExtendedData(); } + if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitionCollection(this); } + + return (_data.RowDefinitions); + } + } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + /// + /// Derived class must implement to support Visual children. The method must return + /// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1. + /// + /// By default a Visual does not have any children. + /// + /// Remark: + /// During this virtual call it is not valid to modify the Visual tree. + /// + protected override Visual GetVisualChild(int index) + { + // because "base.Count + 1" for GridLinesRenderer + // argument checking done at the base class + if(index == base.VisualChildrenCount) + { + if (_gridLinesRenderer == null) + { + throw new ArgumentOutOfRangeException("index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)); + } + return _gridLinesRenderer; + } + else return base.GetVisualChild(index); + } + + /// + /// Derived classes override this property to enable the Visual code to enumerate + /// the Visual children. Derived classes need to return the number of children + /// from this method. + /// + /// By default a Visual does not have any children. + /// + /// Remark: During this virtual method the Visual tree must not be modified. + /// + protected override int VisualChildrenCount + { + //since GridLinesRenderer has not been added as a child, so we do not subtract + get { return base.VisualChildrenCount + (_gridLinesRenderer != null ? 1 : 0); } + } + + + /// + /// Content measurement. + /// + /// Constraint + /// Desired size + protected override Size MeasureOverride(Size constraint) + { + Size gridDesiredSize; + ExtendedData extData = ExtData; + + try + { + EnterCounterScope(Counters.MeasureOverride); + + ListenToNotifications = true; + MeasureOverrideInProgress = true; + + if (extData == null) + { + gridDesiredSize = new Size(); + UIElementCollection children = InternalChildren; + + for (int i = 0, count = children.Count; i < count; ++i) + { + UIElement child = children[i]; + if (child != null) + { + child.Measure(constraint); + gridDesiredSize.Width = Math.Max(gridDesiredSize.Width, child.DesiredSize.Width); + gridDesiredSize.Height = Math.Max(gridDesiredSize.Height, child.DesiredSize.Height); + } + } + } + else + { + { + bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); + bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); + + // Clear index information and rounding errors + if (RowDefinitionCollectionDirty || ColumnDefinitionCollectionDirty) + { + if (_definitionIndices != null) + { + Array.Clear(_definitionIndices, 0, _definitionIndices.Length); + _definitionIndices = null; + } + + if (UseLayoutRounding) + { + if (_roundingErrors != null) + { + Array.Clear(_roundingErrors, 0, _roundingErrors.Length); + _roundingErrors = null; + } + } + } + + ValidateDefinitionsUStructure(); + ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); + + ValidateDefinitionsVStructure(); + ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); + + CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); + + SizeToContentU = sizeToContentU; + SizeToContentV = sizeToContentV; + } + + ValidateCells(); + + Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); + + // Grid classifies cells into four groups depending on + // the column / row type a cell belongs to (number corresponds to + // group number): + // + // Px Auto Star + // +--------+--------+--------+ + // | | | | + // Px | 1 | 1 | 3 | + // | | | | + // +--------+--------+--------+ + // | | | | + // Auto | 1 | 1 | 3 | + // | | | | + // +--------+--------+--------+ + // | | | | + // Star | 4 | 2 | 4 | + // | | | | + // +--------+--------+--------+ + // + // The group number indicates the order in which cells are measured. + // Certain order is necessary to be able to dynamically resolve star + // columns / rows sizes which are used as input for measuring of + // the cells belonging to them. + // + // However, there are cases when topology of a grid causes cyclical + // size dependences. For example: + // + // + // column width="Auto" column width="*" + // +----------------------+----------------------+ + // | | | + // | | | + // | | | + // | | | + // row height="Auto" | | cell 1 2 | + // | | | + // | | | + // | | | + // | | | + // +----------------------+----------------------+ + // | | | + // | | | + // | | | + // | | | + // row height="*" | cell 2 1 | | + // | | | + // | | | + // | | | + // | | | + // +----------------------+----------------------+ + // + // In order to accurately calculate constraint width for "cell 1 2" + // (which is the remaining of grid's available width and calculated + // value of Auto column), "cell 2 1" needs to be calculated first, + // as it contributes to the Auto column's calculated value. + // At the same time in order to accurately calculate constraint + // height for "cell 2 1", "cell 1 2" needs to be calcualted first, + // as it contributes to Auto row height, which is used in the + // computation of Star row resolved height. + // + // to "break" this cyclical dependency we are making (arbitrary) + // decision to treat cells like "cell 2 1" as if they appear in Auto + // rows. And then recalculate them one more time when star row + // heights are resolved. + // + // (Or more strictly) the code below implement the following logic: + // + // +---------+ + // | enter | + // +---------+ + // | + // V + // +----------------+ + // | Measure Group1 | + // +----------------+ + // | + // V + // / - \ + // / \ + // Y / Can \ N + // +--------| Resolve |-----------+ + // | \ StarsV? / | + // | \ / | + // | \ - / | + // V V + // +----------------+ / - \ + // | Resolve StarsV | / \ + // +----------------+ Y / Can \ N + // | +----| Resolve |------+ + // V | \ StarsU? / | + // +----------------+ | \ / | + // | Measure Group2 | | \ - / | + // +----------------+ | V + // | | +-----------------+ + // V | | Measure Group2' | + // +----------------+ | +-----------------+ + // | Resolve StarsU | | | + // +----------------+ V V + // | +----------------+ +----------------+ + // V | Resolve StarsU | | Resolve StarsU | + // +----------------+ +----------------+ +----------------+ + // | Measure Group3 | | | + // +----------------+ V V + // | +----------------+ +----------------+ + // | | Measure Group3 | | Measure Group3 | + // | +----------------+ +----------------+ + // | | | + // | V V + // | +----------------+ +----------------+ + // | | Resolve StarsV | | Resolve StarsV | + // | +----------------+ +----------------+ + // | | | + // | | V + // | | +------------------+ + // | | | Measure Group2'' | + // | | +------------------+ + // | | | + // +----------------------+-------------------------+ + // | + // V + // +----------------+ + // | Measure Group4 | + // +----------------+ + // | + // V + // +--------+ + // | exit | + // +--------+ + // + // where: + // * all [Measure GroupN] - regular children measure process - + // each cell is measured given contraint size as an input + // and each cell's desired size is accumulated on the + // corresponding column / row; + // * [Measure Group2'] - is when each cell is measured with + // infinit height as a constraint and a cell's desired + // height is ignored; + // * [Measure Groups''] - is when each cell is measured (second + // time during single Grid.MeasureOverride) regularly but its + // returned width is ignored; + // + // This algorithm is believed to be as close to ideal as possible. + // It has the following drawbacks: + // * cells belonging to Group2 can be called to measure twice; + // * iff during second measure a cell belonging to Group2 returns + // desired width greater than desired width returned the first + // time, such a cell is going to be clipped, even though it + // appears in Auto column. + // + + MeasureCellsGroup(extData.CellGroup1, constraint, false, false); + + { + // after Group1 is measured, only Group3 may have cells belonging to Auto rows. + bool canResolveStarsV = !HasGroup3CellsInAutoRows; + + if (canResolveStarsV) + { + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + MeasureCellsGroup(extData.CellGroup2, constraint, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + } + else + { + // if at least one cell exists in Group2, it must be measured before + // StarsU can be resolved. + bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length; + if (canResolveStarsU) + { + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + } + else + { + // This is a revision to the algorithm employed for the cyclic + // dependency case described above. We now repeatedly + // measure Group3 and Group2 until their sizes settle. We + // also use a count heuristic to break a loop in case of one. + + bool hasDesiredSizeUChanged = false; + int cnt=0; + + // Cache Group2MinWidths & Group3MinHeights + double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); + double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true); + + MeasureCellsGroup(extData.CellGroup2, constraint, false, true); + + do + { + if (hasDesiredSizeUChanged) + { + // Reset cached Group3Heights + ApplyCachedMinSizes(group3MinSizes, true); + } + + if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + + // Reset cached Group2Widths + ApplyCachedMinSizes(group2MinSizes, false); + + if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + } + while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); + } + } + } + + MeasureCellsGroup(extData.CellGroup4, constraint, false, false); + + EnterCounter(Counters._CalculateDesiredSize); + gridDesiredSize = new Size( + CalculateDesiredSize(DefinitionsU), + CalculateDesiredSize(DefinitionsV)); + ExitCounter(Counters._CalculateDesiredSize); + } + } + finally + { + MeasureOverrideInProgress = false; + ExitCounterScope(Counters.MeasureOverride); + } + + return (gridDesiredSize); + } + + /// + /// Content arrangement. + /// + /// Arrange size + protected override Size ArrangeOverride(Size arrangeSize) + { + try + { + EnterCounterScope(Counters.ArrangeOverride); + + ArrangeOverrideInProgress = true; + + if (_data == null) + { + UIElementCollection children = InternalChildren; + + for (int i = 0, count = children.Count; i < count; ++i) + { + UIElement child = children[i]; + if (child != null) + { + child.Arrange(new Rect(arrangeSize)); + } + } + } + else + { + Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); + + EnterCounter(Counters._SetFinalSize); + + SetFinalSize(DefinitionsU, arrangeSize.Width, true); + SetFinalSize(DefinitionsV, arrangeSize.Height, false); + + ExitCounter(Counters._SetFinalSize); + + UIElementCollection children = InternalChildren; + + for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) + { + UIElement cell = children[currentCell]; + if (cell == null) + { + continue; + } + + int columnIndex = PrivateCells[currentCell].ColumnIndex; + int rowIndex = PrivateCells[currentCell].RowIndex; + int columnSpan = PrivateCells[currentCell].ColumnSpan; + int rowSpan = PrivateCells[currentCell].RowSpan; + + Rect cellRect = new Rect( + columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, + rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, + GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), + GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan) ); + + EnterCounter(Counters._ArrangeChildHelper2); + cell.Arrange(cellRect); + ExitCounter(Counters._ArrangeChildHelper2); + } + + // update render bound on grid lines renderer visual + GridLinesRenderer gridLinesRenderer = EnsureGridLinesRenderer(); + if (gridLinesRenderer != null) + { + gridLinesRenderer.UpdateRenderBounds(arrangeSize); + } + } + } + finally + { + SetValid(); + ArrangeOverrideInProgress = false; + ExitCounterScope(Counters.ArrangeOverride); + } + return (arrangeSize); + } + + /// + /// + /// + protected internal override void OnVisualChildrenChanged( + DependencyObject visualAdded, + DependencyObject visualRemoved) + { + CellsStructureDirty = true; + + base.OnVisualChildrenChanged(visualAdded, visualRemoved); + } + + #endregion Protected Methods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + /// + /// Invalidates grid caches and makes the grid dirty for measure. + /// + internal void Invalidate() + { + CellsStructureDirty = true; + InvalidateMeasure(); + } + + /// + /// Returns final width for a column. + /// + /// + /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. + /// + internal double GetFinalColumnDefinitionWidth(int columnIndex) + { + double value = 0.0; + + Invariant.Assert(_data != null); + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionCollectionDirty) + { + DefinitionBase[] definitions = DefinitionsU; + value = definitions[(columnIndex + 1) % definitions.Length].FinalOffset; + if (columnIndex != 0) { value -= definitions[columnIndex].FinalOffset; } + } + return (value); + } + + /// + /// Returns final height for a row. + /// + /// + /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. + /// + internal double GetFinalRowDefinitionHeight(int rowIndex) + { + double value = 0.0; + + Invariant.Assert(_data != null); + + // actual value calculations require structure to be up-to-date + if (!RowDefinitionCollectionDirty) + { + DefinitionBase[] definitions = DefinitionsV; + value = definitions[(rowIndex + 1) % definitions.Length].FinalOffset; + if (rowIndex != 0) { value -= definitions[rowIndex].FinalOffset; } + } + return (value); + } + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + /// + /// Convenience accessor to MeasureOverrideInProgress bit flag. + /// + internal bool MeasureOverrideInProgress + { + get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); } + set { SetFlags(value, Flags.MeasureOverrideInProgress); } + } + + /// + /// Convenience accessor to ArrangeOverrideInProgress bit flag. + /// + internal bool ArrangeOverrideInProgress + { + get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); } + set { SetFlags(value, Flags.ArrangeOverrideInProgress); } + } + + /// + /// Convenience accessor to ValidDefinitionsUStructure bit flag. + /// + internal bool ColumnDefinitionCollectionDirty + { + get { return (!CheckFlagsAnd(Flags.ValidDefinitionsUStructure)); } + set { SetFlags(!value, Flags.ValidDefinitionsUStructure); } + } + + /// + /// Convenience accessor to ValidDefinitionsVStructure bit flag. + /// + internal bool RowDefinitionCollectionDirty + { + get { return (!CheckFlagsAnd(Flags.ValidDefinitionsVStructure)); } + set { SetFlags(!value, Flags.ValidDefinitionsVStructure); } + } + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Private Methods + // + //------------------------------------------------------ + + #region Private Methods + + /// + /// Lays out cells according to rows and columns, and creates lookup grids. + /// + private void ValidateCells() + { + EnterCounter(Counters._ValidateCells); + + if (CellsStructureDirty) + { + ValidateCellsCore(); + CellsStructureDirty = false; + } + + ExitCounter(Counters._ValidateCells); + } + + /// + /// ValidateCellsCore + /// + private void ValidateCellsCore() + { + UIElementCollection children = InternalChildren; + ExtendedData extData = ExtData; + + extData.CellCachesCollection = new CellCache[children.Count]; + extData.CellGroup1 = int.MaxValue; + extData.CellGroup2 = int.MaxValue; + extData.CellGroup3 = int.MaxValue; + extData.CellGroup4 = int.MaxValue; + + bool hasStarCellsU = false; + bool hasStarCellsV = false; + bool hasGroup3CellsInAutoRows = false; + + for (int i = PrivateCells.Length - 1; i >= 0; --i) + { + UIElement child = children[i]; + if (child == null) + { + continue; + } + + CellCache cell = new CellCache(); + + // + // read and cache child positioning properties + // + + // read indices from the corresponding properties + // clamp to value < number_of_columns + // column >= 0 is guaranteed by property value validation callback + cell.ColumnIndex = Math.Min(GetColumn(child), DefinitionsU.Length - 1); + // clamp to value < number_of_rows + // row >= 0 is guaranteed by property value validation callback + cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Length - 1); + + // read span properties + // clamp to not exceed beyond right side of the grid + // column_span > 0 is guaranteed by property value validation callback + cell.ColumnSpan = Math.Min(GetColumnSpan(child), DefinitionsU.Length - cell.ColumnIndex); + + // clamp to not exceed beyond bottom side of the grid + // row_span > 0 is guaranteed by property value validation callback + cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Length - cell.RowIndex); + + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Length); + Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Length); + + // + // calculate and cache length types for the child + // + + cell.SizeTypeU = GetLengthTypeForRange(DefinitionsU, cell.ColumnIndex, cell.ColumnSpan); + cell.SizeTypeV = GetLengthTypeForRange(DefinitionsV, cell.RowIndex, cell.RowSpan); + + hasStarCellsU |= cell.IsStarU; + hasStarCellsV |= cell.IsStarV; + + // + // distribute cells into four groups. + // + + if (!cell.IsStarV) + { + if (!cell.IsStarU) + { + cell.Next = extData.CellGroup1; + extData.CellGroup1 = i; + } + else + { + cell.Next = extData.CellGroup3; + extData.CellGroup3 = i; + + // remember if this cell belongs to auto row + hasGroup3CellsInAutoRows |= cell.IsAutoV; + } + } + else + { + if ( cell.IsAutoU + // note below: if spans through Star column it is NOT Auto + && !cell.IsStarU ) + { + cell.Next = extData.CellGroup2; + extData.CellGroup2 = i; + } + else + { + cell.Next = extData.CellGroup4; + extData.CellGroup4 = i; + } + } + + PrivateCells[i] = cell; + } + + HasStarCellsU = hasStarCellsU; + HasStarCellsV = hasStarCellsV; + HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; + } + + /// + /// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection + /// or to a default single element collection. DefinitionsU gets trimmed to size. + /// + /// + /// This is one of two methods, where ColumnDefinitions and DefinitionsU are directly accessed. + /// All the rest measure / arrange / render code must use DefinitionsU. + /// + private void ValidateDefinitionsUStructure() + { + EnterCounter(Counters._ValidateColsStructure); + + if (ColumnDefinitionCollectionDirty) + { + ExtendedData extData = ExtData; + + if (extData.ColumnDefinitions == null) + { + if (extData.DefinitionsU == null) + { + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() }; + } + } + else + { + extData.ColumnDefinitions.InternalTrimToSize(); + + if (extData.ColumnDefinitions.InternalCount == 0) + { + // if column definitions collection is empty + // mockup array with one column + extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() }; + } + else + { + extData.DefinitionsU = extData.ColumnDefinitions.InternalItems; + } + } + + ColumnDefinitionCollectionDirty = false; + } + + Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Length > 0); + + ExitCounter(Counters._ValidateColsStructure); + } + + /// + /// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection + /// or to a default single element collection. DefinitionsV gets trimmed to size. + /// + /// + /// This is one of two methods, where RowDefinitions and DefinitionsV are directly accessed. + /// All the rest measure / arrange / render code must use DefinitionsV. + /// + private void ValidateDefinitionsVStructure() + { + EnterCounter(Counters._ValidateRowsStructure); + + if (RowDefinitionCollectionDirty) + { + ExtendedData extData = ExtData; + + if (extData.RowDefinitions == null) + { + if (extData.DefinitionsV == null) + { + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() }; + } + } + else + { + extData.RowDefinitions.InternalTrimToSize(); + + if (extData.RowDefinitions.InternalCount == 0) + { + // if row definitions collection is empty + // mockup array with one row + extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() }; + } + else + { + extData.DefinitionsV = extData.RowDefinitions.InternalItems; + } + } + + RowDefinitionCollectionDirty = false; + } + + Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Length > 0); + + ExitCounter(Counters._ValidateRowsStructure); + } + + /// + /// Validates layout time size type information on given array of definitions. + /// Sets MinSize and MeasureSizes. + /// + /// Array of definitions to update. + /// if "true" then star definitions are treated as Auto. + private void ValidateDefinitionsLayout( + DefinitionBase[] definitions, + bool treatStarAsAuto) + { + for (int i = 0; i < definitions.Length; ++i) + { + definitions[i].OnBeforeLayout(this); + + double userMinSize = definitions[i].UserMinSize; + double userMaxSize = definitions[i].UserMaxSize; + double userSize = 0; + + switch (definitions[i].UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + definitions[i].SizeType = LayoutTimeSizeType.Pixel; + userSize = definitions[i].UserSize.Value; + // this was brought with NewLayout and defeats squishy behavior + userMinSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + break; + case (GridUnitType.Auto): + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + break; + case (GridUnitType.Star): + if (treatStarAsAuto) + { + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + } + else + { + definitions[i].SizeType = LayoutTimeSizeType.Star; + userSize = double.PositiveInfinity; + } + break; + default: + Debug.Assert(false); + break; + } + + definitions[i].UpdateMinSize(userMinSize); + definitions[i].MeasureSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + } + } + + private double[] CacheMinSizes(int cellsHead, bool isRows) + { + double[] minSizes = isRows ? new double[DefinitionsV.Length] : new double[DefinitionsU.Length]; + + for (int j=0; j + /// Measures one group of cells. + /// + /// Head index of the cells chain. + /// Reference size for spanned cells + /// calculations. + /// When "true" cells' desired + /// width is not registered in columns. + /// Passed through to MeasureCell. + /// When "true" cells' desired height is not registered in rows. + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV, + out bool hasDesiredSizeUChanged) + { + hasDesiredSizeUChanged = false; + + if (cellsHead >= PrivateCells.Length) + { + return; + } + + UIElementCollection children = InternalChildren; + Hashtable spanStore = null; + bool ignoreDesiredSizeV = forceInfinityV; + + int i = cellsHead; + do + { + double oldWidth = children[i].DesiredSize.Width; + + MeasureCell(i, forceInfinityV); + + hasDesiredSizeUChanged |= !DoubleUtil.AreClose(oldWidth, children[i].DesiredSize.Width); + + if (!ignoreDesiredSizeU) + { + if (PrivateCells[i].ColumnSpan == 1) + { + DefinitionsU[PrivateCells[i].ColumnIndex].UpdateMinSize(Math.Min(children[i].DesiredSize.Width, DefinitionsU[PrivateCells[i].ColumnIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + PrivateCells[i].ColumnIndex, + PrivateCells[i].ColumnSpan, + true, + children[i].DesiredSize.Width); + } + } + + if (!ignoreDesiredSizeV) + { + if (PrivateCells[i].RowSpan == 1) + { + DefinitionsV[PrivateCells[i].RowIndex].UpdateMinSize(Math.Min(children[i].DesiredSize.Height, DefinitionsV[PrivateCells[i].RowIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + PrivateCells[i].RowIndex, + PrivateCells[i].RowSpan, + false, + children[i].DesiredSize.Height); + } + } + + i = PrivateCells[i].Next; + } while (i < PrivateCells.Length); + + if (spanStore != null) + { + foreach (DictionaryEntry e in spanStore) + { + SpanKey key = (SpanKey)e.Key; + double requestedSize = (double)e.Value; + + EnsureMinSizeInDefinitionRange( + key.U ? DefinitionsU : DefinitionsV, + key.Start, + key.Count, + requestedSize, + key.U ? referenceSize.Width : referenceSize.Height); + } + } + } + + /// + /// Helper method to register a span information for delayed processing. + /// + /// Reference to a hashtable object used as storage. + /// Span starting index. + /// Span count. + /// true if this is a column span. false if this is a row span. + /// Value to store. If an entry already exists the biggest value is stored. + private static void RegisterSpan( + ref Hashtable store, + int start, + int count, + bool u, + double value) + { + if (store == null) + { + store = new Hashtable(); + } + + SpanKey key = new SpanKey(start, count, u); + object o = store[key]; + + if ( o == null + || value > (double)o ) + { + store[key] = value; + } + } + + /// + /// Takes care of measuring a single cell. + /// + /// Index of the cell to measure. + /// If "true" then cell is always + /// calculated to infinite height. + private void MeasureCell( + int cell, + bool forceInfinityV) + { + EnterCounter(Counters._MeasureCell); + + double cellMeasureWidth; + double cellMeasureHeight; + + if ( PrivateCells[cell].IsAutoU + && !PrivateCells[cell].IsStarU ) + { + // if cell belongs to at least one Auto column and not a single Star column + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureWidth = double.PositiveInfinity; + } + else + { + // otherwise... + cellMeasureWidth = GetMeasureSizeForRange( + DefinitionsU, + PrivateCells[cell].ColumnIndex, + PrivateCells[cell].ColumnSpan); + } + + if (forceInfinityV) + { + cellMeasureHeight = double.PositiveInfinity; + } + else if ( PrivateCells[cell].IsAutoV + && !PrivateCells[cell].IsStarV ) + { + // if cell belongs to at least one Auto row and not a single Star row + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureHeight = double.PositiveInfinity; + } + else + { + cellMeasureHeight = GetMeasureSizeForRange( + DefinitionsV, + PrivateCells[cell].RowIndex, + PrivateCells[cell].RowSpan); + } + + EnterCounter(Counters.__MeasureChild); + UIElement child = InternalChildren[cell]; + if (child != null) + { + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); + } + ExitCounter(Counters.__MeasureChild); + + ExitCounter(Counters._MeasureCell); + } + + + /// + /// Calculates one dimensional measure size for given definitions' range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Calculated measure size. + /// + /// For "Auto" definitions MinWidth is used in place of PreferredSize. + /// + private double GetMeasureSizeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); + + double measureSize = 0; + int i = start + count - 1; + + do + { + measureSize += (definitions[i].SizeType == LayoutTimeSizeType.Auto) + ? definitions[i].MinSize + : definitions[i].MeasureSize; + } while (--i >= start); + + return (measureSize); + } + + /// + /// Accumulates length type information for given definition's range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Length type for given range. + private LayoutTimeSizeType GetLengthTypeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); + + LayoutTimeSizeType lengthType = LayoutTimeSizeType.None; + int i = start + count - 1; + + do + { + lengthType |= definitions[i].SizeType; + } while (--i >= start); + + return (lengthType); + } + + /// + /// Distributes min size back to definition array's range. + /// + /// Start of the range. + /// Number of items in the range. + /// Minimum size that should "fit" into the definitions range. + /// Definition array receiving distribution. + /// Size used to resolve percentages. + private void EnsureMinSizeInDefinitionRange( + DefinitionBase[] definitions, + int start, + int count, + double requestedSize, + double percentReferenceSize) + { + Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Length); + + // avoid processing when asked to distribute "0" + if (!_IsZero(requestedSize)) + { + DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting + int end = start + count; + int autoDefinitionsCount = 0; + double rangeMinSize = 0; + double rangePreferredSize = 0; + double rangeMaxSize = 0; + double maxMaxSize = 0; // maximum of maximum sizes + + // first accumulate the necessary information: + // a) sum up the sizes in the range; + // b) count the number of auto definitions in the range; + // c) initialize temp array + // d) cache the maximum size into SizeCache + // e) accumulate max of max sizes + for (int i = start; i < end; ++i) + { + double minSize = definitions[i].MinSize; + double preferredSize = definitions[i].PreferredSize; + double maxSize = Math.Max(definitions[i].UserMaxSize, minSize); + + rangeMinSize += minSize; + rangePreferredSize += preferredSize; + rangeMaxSize += maxSize; + + definitions[i].SizeCache = maxSize; + + // sanity check: no matter what, but min size must always be the smaller; + // max size must be the biggest; and preferred should be in between + Debug.Assert( minSize <= preferredSize + && preferredSize <= maxSize + && rangeMinSize <= rangePreferredSize + && rangePreferredSize <= rangeMaxSize ); + + if (maxMaxSize < maxSize) maxMaxSize = maxSize; + if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; + tempDefinitions[i - start] = definitions[i]; + } + + // avoid processing if the range already big enough + if (requestedSize > rangeMinSize) + { + if (requestedSize <= rangePreferredSize) + { + // + // requestedSize fits into preferred size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions - they should continue to stay "tight"; + // * for all non-auto definitions distribute to equi-size min sizes, without exceeding preferred size. + // + // in order to achieve that, definitions are sorted in a way that all auto definitions + // are first, then definitions follow ascending order with PreferredSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, s_spanPreferredDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + // adjust sizeToDistribute value by subtracting auto definition min size + sizeToDistribute -= (tempDefinitions[i].MinSize); + } + + for (; i < count; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); + if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } + sizeToDistribute -= newMinSize; + } + + // sanity check: requested size must all be distributed + Debug.Assert(_IsZero(sizeToDistribute)); + } + else if (requestedSize <= rangeMaxSize) + { + // + // requestedSize bigger than preferred size, but fit into max size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions, if possible - they should continue to stay "tight"; + // * for all non-auto definitions distribute to euqi-size min sizes, without exceeding max size. + // + // in order to achieve that, definitions are sorted in a way that all non-auto definitions + // are last, then definitions follow ascending order with MaxSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, s_spanMaxDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].PreferredSize; + double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + for (; i < count; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].MinSize; + double newMinSize = preferredSize + sizeToDistribute / (count - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + // sanity check: requested size must all be distributed + Debug.Assert(_IsZero(sizeToDistribute)); + } + else + { + // + // requestedSize bigger than max size of the range. + // distribute according to the following logic: + // * for all definitions distribute to equi-size min sizes. + // + double equalSize = requestedSize / count; + + if ( equalSize < maxMaxSize + && !_AreClose(equalSize, maxMaxSize) ) + { + // equi-size is less than maximum of maxSizes. + // in this case distribute so that smaller definitions grow faster than + // bigger ones. + double totalRemainingSize = maxMaxSize * count - rangeMaxSize; + double sizeToDistribute = requestedSize - rangeMaxSize; + + // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers + Debug.Assert( !double.IsInfinity(totalRemainingSize) + && !DoubleUtil.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !DoubleUtil.IsNaN(sizeToDistribute) + && sizeToDistribute > 0 ); + + for (int i = 0; i < count; ++i) + { + double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + } + } + else + { + // + // equi-size is greater or equal to maximum of max sizes. + // all definitions receive equalSize as their mim sizes. + // + for (int i = 0; i < count; ++i) + { + tempDefinitions[i].UpdateMinSize(equalSize); + } + } + } + } + } + } + + /// + /// Resolves Star's for given array of definitions. + /// + /// Array of definitions to resolve stars. + /// All available size. + /// + /// Must initialize LayoutSize for all Star entries in given array of definitions. + /// + private void ResolveStar( + DefinitionBase[] definitions, + double availableSize) + { + if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) + { + ResolveStarLegacy(definitions, availableSize); + } + else + { + ResolveStarMaxDiscrepancy(definitions, availableSize); + } + } + + // original implementation, used from 3.0 through 4.6.2 + private void ResolveStarLegacy( + DefinitionBase[] definitions, + double availableSize) + { + DefinitionBase[] tempDefinitions = TempDefinitions; + int starDefinitionsCount = 0; + double takenSize = 0; + + for (int i = 0; i < definitions.Length; ++i) + { + switch (definitions[i].SizeType) + { + case (LayoutTimeSizeType.Auto): + takenSize += definitions[i].MinSize; + break; + case (LayoutTimeSizeType.Pixel): + takenSize += definitions[i].MeasureSize; + break; + case (LayoutTimeSizeType.Star): + { + tempDefinitions[starDefinitionsCount++] = definitions[i]; + + double starValue = definitions[i].UserSize.Value; + + if (_IsZero(starValue)) + { + definitions[i].MeasureSize = 0; + definitions[i].SizeCache = 0; + } + else + { + // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values + // can be summed up without overflow + starValue = Math.Min(starValue, c_starClip); + + // Note: normalized star value is temporary cached into MeasureSize + definitions[i].MeasureSize = starValue; + double maxSize = Math.Max(definitions[i].MinSize, definitions[i].UserMaxSize); + maxSize = Math.Min(maxSize, c_starClip); + definitions[i].SizeCache = maxSize / starValue; + } + } + break; + } + } + + if (starDefinitionsCount > 0) + { + Array.Sort(tempDefinitions, 0, starDefinitionsCount, s_starDistributionOrderComparer); + + // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... + // partial sum value is stored in each definition's SizeCache member. + // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus + // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. + // this is an important change from previous implementation where the following was possible: + // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... + double allStarWeights = 0; + int i = starDefinitionsCount - 1; + do + { + allStarWeights += tempDefinitions[i].MeasureSize; + tempDefinitions[i].SizeCache = allStarWeights; + } while (--i >= 0); + + i = 0; + do + { + double resolvedSize; + double starValue = tempDefinitions[i].MeasureSize; + + if (_IsZero(starValue)) + { + resolvedSize = tempDefinitions[i].MinSize; + } + else + { + double userSize = Math.Max(availableSize - takenSize, 0.0) * (starValue / tempDefinitions[i].SizeCache); + resolvedSize = Math.Min(userSize, tempDefinitions[i].UserMaxSize); + resolvedSize = Math.Max(tempDefinitions[i].MinSize, resolvedSize); + } + + tempDefinitions[i].MeasureSize = resolvedSize; + takenSize += resolvedSize; + } while (++i < starDefinitionsCount); + } + } + + // new implementation as of 4.7. Several improvements: + // 1. Allocate to *-defs hitting their min or max constraints, before allocating + // to other *-defs. A def that hits its min uses more space than its + // proportional share, reducing the space available to everyone else. + // The legacy algorithm deducted this space only from defs processed + // after the min; the new algorithm deducts it proportionally from all + // defs. This avoids the "*-defs exceed available space" problem, + // and other related problems where *-defs don't receive proportional + // allocations even though no constraints are preventing it. + // 2. When multiple defs hit min or max, resolve the one with maximum + // discrepancy (defined below). This avoids discontinuities - small + // change in available space resulting in large change to one def's allocation. + // 3. Correct handling of large *-values, including Infinity. + private void ResolveStarMaxDiscrepancy( + DefinitionBase[] definitions, + double availableSize) + { + int defCount = definitions.Length; + DefinitionBase[] tempDefinitions = TempDefinitions; + int minCount = 0, maxCount = 0; + double takenSize = 0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i=0; i maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (Double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > Double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(Double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3=true; runPhase2and3; ) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of tempDefinitions, + // the "max" list in the second half. TempDefinitions has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i=0; i 0.0) + { + // store ratio w/min in MeasureSize (for now) + tempDefinitions[minCount++] = def; + def.MeasureSize = starWeight / def.MinSize; + } + + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + if (!Double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + tempDefinitions[defCount + maxCount++] = def; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + break; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = availableSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + Array.Sort(tempDefinitions, 0, minCount, s_minRatioComparer); + Array.Sort(tempDefinitions, defCount, maxCount, s_maxRatioComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.SizeType == LayoutTimeSizeType.Star && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : Double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedDef = tempDefinitions[minCount - 1]; + resolvedSize = resolvedDef.MinSize; + --minCount; + } + else + { + resolvedDef = tempDefinitions[defCount + maxCount - 1]; + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = availableSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) + { + --minCount; + tempDefinitions[minCount] = null; + } + while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) + { + --maxCount; + tempDefinitions[defCount + maxCount] = null; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < availableSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > availableSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[defCount + i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i=0; i 0) + { + Array.Sort(tempDefinitions, 0, starCount, s_starWeightComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = tempDefinitions[i]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = tempDefinitions[i]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSize, resolvedSize); + + def.MeasureSize = resolvedSize; + takenSize += resolvedSize; + } + } + } + + /// + /// Calculates desired size for given array of definitions. + /// + /// Array of definitions to use for calculations. + /// Desired size. + private double CalculateDesiredSize( + DefinitionBase[] definitions) + { + double desiredSize = 0; + + for (int i = 0; i < definitions.Length; ++i) + { + desiredSize += definitions[i].MinSize; + } + + return (desiredSize); + } + + /// + /// Calculates and sets final size for all definitions in the given array. + /// + /// Array of definitions to process. + /// Final size to lay out to. + /// True if sizing row definitions, false for columns + private void SetFinalSize( + DefinitionBase[] definitions, + double finalSize, + bool columns) + { + if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) + { + SetFinalSizeLegacy(definitions, finalSize, columns); + } + else + { + SetFinalSizeMaxDiscrepancy(definitions, finalSize, columns); + } + } + + // original implementation, used from 3.0 through 4.6.2 + private void SetFinalSizeLegacy( + DefinitionBase[] definitions, + double finalSize, + bool columns) + { + int starDefinitionsCount = 0; // traverses form the first entry up + int nonStarIndex = definitions.Length; // traverses from the last entry down + double allPreferredArrangeSize = 0; + bool useLayoutRounding = this.UseLayoutRounding; + int[] definitionIndices = DefinitionIndices; + double[] roundingErrors = null; + + // If using layout rounding, check whether rounding needs to compensate for high DPI + double dpi = 1.0; + + if (useLayoutRounding) + { + DpiScale dpiScale = GetDpi(); + dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; + roundingErrors = RoundingErrors; + } + + for (int i = 0; i < definitions.Length; ++i) + { + // if definition is shared then is cannot be star + Debug.Assert(!definitions[i].IsShared || !definitions[i].UserSize.IsStar); + + if (definitions[i].UserSize.IsStar) + { + double starValue = definitions[i].UserSize.Value; + + if (_IsZero(starValue)) + { + // cach normilized star value temporary into MeasureSize + definitions[i].MeasureSize = 0; + definitions[i].SizeCache = 0; + } + else + { + // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values + // can be summed up without overflow + starValue = Math.Min(starValue, c_starClip); + + // Note: normalized star value is temporary cached into MeasureSize + definitions[i].MeasureSize = starValue; + double maxSize = Math.Max(definitions[i].MinSizeForArrange, definitions[i].UserMaxSize); + maxSize = Math.Min(maxSize, c_starClip); + definitions[i].SizeCache = maxSize / starValue; + if (useLayoutRounding) + { + roundingErrors[i] = definitions[i].SizeCache; + definitions[i].SizeCache = UIElement.RoundLayoutValue(definitions[i].SizeCache, dpi); + } + } + definitionIndices[starDefinitionsCount++] = i; + } + else + { + double userSize = 0; + + switch (definitions[i].UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + userSize = definitions[i].UserSize.Value; + break; + + case (GridUnitType.Auto): + userSize = definitions[i].MinSizeForArrange; + break; + } + + double userMaxSize; + + if (definitions[i].IsShared) + { + // overriding userMaxSize effectively prevents squishy-ness. + // this is a "solution" to avoid shared definitions from been sized to + // different final size at arrange time, if / when different grids receive + // different final sizes. + userMaxSize = userSize; + } + else + { + userMaxSize = definitions[i].UserMaxSize; + } + + definitions[i].SizeCache = Math.Max(definitions[i].MinSizeForArrange, Math.Min(userSize, userMaxSize)); + if (useLayoutRounding) + { + roundingErrors[i] = definitions[i].SizeCache; + definitions[i].SizeCache = UIElement.RoundLayoutValue(definitions[i].SizeCache, dpi); + } + + allPreferredArrangeSize += definitions[i].SizeCache; + definitionIndices[--nonStarIndex] = i; + } + } + + // indices should meet + Debug.Assert(nonStarIndex == starDefinitionsCount); + + if (starDefinitionsCount > 0) + { + StarDistributionOrderIndexComparer starDistributionOrderIndexComparer = new StarDistributionOrderIndexComparer(definitions); + Array.Sort(definitionIndices, 0, starDefinitionsCount, starDistributionOrderIndexComparer); + + // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... + // partial sum value is stored in each definition's SizeCache member. + // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus + // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. + // this is an important change from previous implementation where the following was possible: + // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... + double allStarWeights = 0; + int i = starDefinitionsCount - 1; + do + { + allStarWeights += definitions[definitionIndices[i]].MeasureSize; + definitions[definitionIndices[i]].SizeCache = allStarWeights; + } while (--i >= 0); + + i = 0; + do + { + double resolvedSize; + double starValue = definitions[definitionIndices[i]].MeasureSize; + + if (_IsZero(starValue)) + { + resolvedSize = definitions[definitionIndices[i]].MinSizeForArrange; + } + else + { + double userSize = Math.Max(finalSize - allPreferredArrangeSize, 0.0) * (starValue / definitions[definitionIndices[i]].SizeCache); + resolvedSize = Math.Min(userSize, definitions[definitionIndices[i]].UserMaxSize); + resolvedSize = Math.Max(definitions[definitionIndices[i]].MinSizeForArrange, resolvedSize); + } + + definitions[definitionIndices[i]].SizeCache = resolvedSize; + if (useLayoutRounding) + { + roundingErrors[definitionIndices[i]] = definitions[definitionIndices[i]].SizeCache; + definitions[definitionIndices[i]].SizeCache = UIElement.RoundLayoutValue(definitions[definitionIndices[i]].SizeCache, dpi); + } + + allPreferredArrangeSize += definitions[definitionIndices[i]].SizeCache; + } while (++i < starDefinitionsCount); + } + + if ( allPreferredArrangeSize > finalSize + && !_AreClose(allPreferredArrangeSize, finalSize) ) + { + DistributionOrderIndexComparer distributionOrderIndexComparer = new DistributionOrderIndexComparer(definitions); + Array.Sort(definitionIndices, 0, definitions.Length, distributionOrderIndexComparer); + double sizeToDistribute = finalSize - allPreferredArrangeSize; + + for (int i = 0; i < definitions.Length; ++i) + { + int definitionIndex = definitionIndices[i]; + double final = definitions[definitionIndex].SizeCache + (sizeToDistribute / (definitions.Length - i)); + double finalOld = final; + final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); + final = Math.Min(final, definitions[definitionIndex].SizeCache); + + if (useLayoutRounding) + { + roundingErrors[definitionIndex] = final; + final = UIElement.RoundLayoutValue(finalOld, dpi); + final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); + final = Math.Min(final, definitions[definitionIndex].SizeCache); + } + + sizeToDistribute -= (final - definitions[definitionIndex].SizeCache); + definitions[definitionIndex].SizeCache = final; + } + + allPreferredArrangeSize = finalSize - sizeToDistribute; + } + + if (useLayoutRounding) + { + if (!_AreClose(allPreferredArrangeSize, finalSize)) + { + // Compute deltas + for (int i = 0; i < definitions.Length; ++i) + { + roundingErrors[i] = roundingErrors[i] - definitions[i].SizeCache; + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); + double adjustedSize = allPreferredArrangeSize; + double dpiIncrement = UIElement.RoundLayoutValue(1.0, dpi); + + if (allPreferredArrangeSize > finalSize) + { + int i = definitions.Length - 1; + while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (allPreferredArrangeSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Length) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final > definition.SizeCache) + { + adjustedSize += dpiIncrement; + } + definition.SizeCache = final; + i++; + } + } + } + } + + definitions[0].FinalOffset = 0.0; + for (int i = 0; i < definitions.Length; ++i) + { + definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; + } + } + + // new implementation, as of 4.7. This incorporates the same algorithm + // as in ResolveStarMaxDiscrepancy. It differs in the same way that SetFinalSizeLegacy + // differs from ResolveStarLegacy, namely (a) leaves results in def.SizeCache + // instead of def.MeasureSize, (b) implements LayoutRounding if requested, + // (c) stores intermediate results differently. + // The LayoutRounding logic is improved: + // 1. Use pre-rounded values during proportional allocation. This avoids the + // same kind of problems arising from interaction with min/max that + // motivated the new algorithm in the first place. + // 2. Use correct "nudge" amount when distributing roundoff space. This + // comes into play at high DPI - greater than 134. + // 3. Applies rounding only to real pixel values (not to ratios) + private void SetFinalSizeMaxDiscrepancy( + DefinitionBase[] definitions, + double finalSize, + bool columns) + { + int defCount = definitions.Length; + int[] definitionIndices = DefinitionIndices; + int minCount = 0, maxCount = 0; + double takenSize = 0.0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i=0; i maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (Double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > Double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(Double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3=true; runPhase2and3; ) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of definitionIndices, + // the "max" list in the second half. DefinitionIndices has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i=0; i 0.0) + { + // store ratio w/min in MeasureSize (for now) + definitionIndices[minCount++] = i; + def.MeasureSize = starWeight / def.MinSizeForArrange; + } + + double effectiveMaxSize = Math.Max(def.MinSizeForArrange, def.UserMaxSize); + if (!Double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + definitionIndices[defCount + maxCount++] = i; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + } + else + { + double userSize = 0; + + switch (def.UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + userSize = def.UserSize.Value; + break; + + case (GridUnitType.Auto): + userSize = def.MinSizeForArrange; + break; + } + + double userMaxSize; + + if (def.IsShared) + { + // overriding userMaxSize effectively prevents squishy-ness. + // this is a "solution" to avoid shared definitions from been sized to + // different final size at arrange time, if / when different grids receive + // different final sizes. + userMaxSize = userSize; + } + else + { + userMaxSize = def.UserMaxSize; + } + + def.SizeCache = Math.Max(def.MinSizeForArrange, Math.Min(userSize, userMaxSize)); + takenSize += def.SizeCache; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = finalSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + + MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); + Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); + MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); + Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.UserSize.IsStar && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : Double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? definitions[definitionIndices[defCount + maxCount - 1]].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + int resolvedIndex; + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedIndex = definitionIndices[minCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = resolvedDef.MinSizeForArrange; + --minCount; + } + else + { + resolvedIndex = definitionIndices[defCount + maxCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = Math.Max(resolvedDef.MinSizeForArrange, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = finalSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && definitions[definitionIndices[minCount - 1]].MeasureSize < 0.0) + { + --minCount; + definitionIndices[minCount] = -1; + } + while (maxCount > 0 && definitions[definitionIndices[defCount + maxCount - 1]].MeasureSize < 0.0) + { + --maxCount; + definitionIndices[defCount + maxCount] = -1; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < finalSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + if (definitionIndices[i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > finalSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + if (definitionIndices[defCount + i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[defCount + i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i=0; i 0) + { + StarWeightIndexComparer starWeightIndexComparer = new StarWeightIndexComparer(definitions); + Array.Sort(definitionIndices, 0, starCount, starWeightIndexComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight. + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(finalSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSizeForArrange, resolvedSize); + + // Use the raw (unrounded) sizes to update takenSize, so that + // proportions are computed in the same terms as in phase 3; + // this avoids errors arising from min/max constraints. + takenSize += resolvedSize; + def.SizeCache = resolvedSize; + } + } + + // Phase 5. Apply layout rounding. We do this after fully allocating + // unrounded sizes, to avoid breaking assumptions in the previous phases + if (UseLayoutRounding) + { + DpiScale dpiScale = GetDpi(); + double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; + double[] roundingErrors = RoundingErrors; + double roundedTakenSize = 0.0; + + // round each of the allocated sizes, keeping track of the deltas + for (int i = 0; i < definitions.Length; ++i) + { + DefinitionBase def = definitions[i]; + double roundedSize = UIElement.RoundLayoutValue(def.SizeCache, dpi); + roundingErrors[i] = (roundedSize - def.SizeCache); + def.SizeCache = roundedSize; + roundedTakenSize += roundedSize; + } + + // The total allocation might differ from finalSize due to rounding + // effects. Tweak the allocations accordingly. + + // Theoretical and historical note. The problem at hand - allocating + // space to columns (or rows) with *-weights, min and max constraints, + // and layout rounding - has a long history. Especially the special + // case of 50 columns with min=1 and available space=435 - allocating + // seats in the U.S. House of Representatives to the 50 states in + // proportion to their population. There are numerous algorithms + // and papers dating back to the 1700's, including the book: + // Balinski, M. and H. Young, Fair Representation, Yale University Press, New Haven, 1982. + // + // One surprising result of all this research is that *any* algorithm + // will suffer from one or more undesirable features such as the + // "population paradox" or the "Alabama paradox", where (to use our terminology) + // increasing the available space by one pixel might actually decrease + // the space allocated to a given column, or increasing the weight of + // a column might decrease its allocation. This is worth knowing + // in case someone complains about this behavior; it's not a bug so + // much as something inherent to the problem. Cite the book mentioned + // above or one of the 100s of references, and resolve as WontFix. + // + // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) + // each being allocated a large number of pixels (~50 or greater), and + // people don't even notice the kind of 1-pixel anomolies that are + // theoretically inevitable, or don't care if they do. At least they shouldn't + // care - no one should be using the results WPF's grid layout to make + // quantitative decisions; its job is to produce a reasonable display, not + // to allocate seats in Congress. + // + // Our algorithm is more susceptible to paradox than the one currently + // used for Congressional allocation ("Huntington-Hill" algorithm), but + // it is faster to run: O(N log N) vs. O(S * N), where N=number of + // definitions, S = number of available pixels. And it produces + // adequate results in practice, as mentioned above. + // + // To reiterate one point: all this only applies when layout rounding + // is in effect. When fractional sizes are allowed, the algorithm + // behaves as well as possible, subject to the min/max constraints + // and precision of floating-point computation. (However, the resulting + // display is subject to anti-aliasing problems. TANSTAAFL.) + + if (!_AreClose(roundedTakenSize, finalSize)) + { + // Compute deltas + for (int i = 0; i < definitions.Length; ++i) + { + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); + double adjustedSize = roundedTakenSize; + double dpiIncrement = 1.0/dpi; + + if (roundedTakenSize > finalSize) + { + int i = definitions.Length - 1; + while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (roundedTakenSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Length) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSizeForArrange); + if (final > definition.SizeCache) + { + adjustedSize += dpiIncrement; + } + definition.SizeCache = final; + i++; + } + } + } + } + + // Phase 6. Compute final offsets + definitions[0].FinalOffset = 0.0; + for (int i = 0; i < definitions.Length; ++i) + { + definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; + } + } + + /// + /// Choose the ratio with maximum discrepancy from the current proportion. + /// Returns: + /// true if proportion fails a min constraint but not a max, or + /// if the min constraint has higher discrepancy + /// false if proportion fails a max constraint but not a min, or + /// if the max constraint has higher discrepancy + /// null if proportion doesn't fail a min or max constraint + /// The discrepancy is the ratio of the proportion to the max- or min-ratio. + /// When both ratios hit the constraint, minRatio < proportion < maxRatio, + /// and the minRatio has higher discrepancy if + /// (proportion / minRatio) > (maxRatio / proportion) + /// + private static bool? Choose(double minRatio, double maxRatio, double proportion) + { + if (minRatio < proportion) + { + if (maxRatio > proportion) + { + // compare proportion/minRatio : maxRatio/proportion, but + // do it carefully to avoid floating-point overflow or underflow + // and divide-by-0. + double minPower = Math.Floor(Math.Log(minRatio, 2.0)); + double maxPower = Math.Floor(Math.Log(maxRatio, 2.0)); + double f = Math.Pow(2.0, Math.Floor((minPower + maxPower) / 2.0)); + if ((proportion / f) * (proportion / f) > (minRatio / f) * (maxRatio / f)) + { + return true; + } + else + { + return false; + } + } + else + { + return true; + } + } + else if (maxRatio > proportion) + { + return false; + } + + return null; + } + + /// + /// Sorts row/column indices by rounding error if layout rounding is applied. + /// + /// Index, rounding error pair + /// Index, rounding error pair + /// 1 if x.Value > y.Value, 0 if equal, -1 otherwise + private static int CompareRoundingErrors(KeyValuePair x, KeyValuePair y) + { + if (x.Value < y.Value) + { + return -1; + } + else if (x.Value > y.Value) + { + return 1; + } + return 0; + } + + /// + /// Calculates final (aka arrange) size for given range. + /// + /// Array of definitions to process. + /// Start of the range. + /// Number of items in the range. + /// Final size. + private double GetFinalSizeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + double size = 0; + int i = start + count - 1; + + do + { + size += definitions[i].SizeCache; + } while (--i >= start); + + return (size); + } + + /// + /// Clears dirty state for the grid and its columns / rows + /// + private void SetValid() + { + ExtendedData extData = ExtData; + if (extData != null) + { +// for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); +// for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); + + if (extData.TempDefinitions != null) + { + // TempDefinitions has to be cleared to avoid "memory leaks" + Array.Clear(extData.TempDefinitions, 0, Math.Max(DefinitionsU.Length, DefinitionsV.Length)); + extData.TempDefinitions = null; + } + } + } + + /// + /// Returns true if ColumnDefinitions collection is not empty + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeColumnDefinitions() + { + ExtendedData extData = ExtData; + return ( extData != null + && extData.ColumnDefinitions != null + && extData.ColumnDefinitions.Count > 0 ); + } + + /// + /// Returns true if RowDefinitions collection is not empty + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeRowDefinitions() + { + ExtendedData extData = ExtData; + return ( extData != null + && extData.RowDefinitions != null + && extData.RowDefinitions.Count > 0 ); + } + + /// + /// Synchronized ShowGridLines property with the state of the grid's visual collection + /// by adding / removing GridLinesRenderer visual. + /// Returns a reference to GridLinesRenderer visual or null. + /// + private GridLinesRenderer EnsureGridLinesRenderer() + { + // + // synchronize the state + // + if (ShowGridLines && (_gridLinesRenderer == null)) + { + _gridLinesRenderer = new GridLinesRenderer(); + this.AddVisualChild(_gridLinesRenderer); + } + + if ((!ShowGridLines) && (_gridLinesRenderer != null)) + { + this.RemoveVisualChild(_gridLinesRenderer); + _gridLinesRenderer = null; + } + + return (_gridLinesRenderer); + } + + /// + /// SetFlags is used to set or unset one or multiple + /// flags on the object. + /// + private void SetFlags(bool value, Flags flags) + { + _flags = value ? (_flags | flags) : (_flags & (~flags)); + } + + /// + /// CheckFlagsAnd returns true if all the flags in the + /// given bitmask are set on the object. + /// + private bool CheckFlagsAnd(Flags flags) + { + return ((_flags & flags) == flags); + } + + /// + /// CheckFlagsOr returns true if at least one flag in the + /// given bitmask is set. + /// + /// + /// If no bits are set in the given bitmask, the method returns + /// true. + /// + private bool CheckFlagsOr(Flags flags) + { + return (flags == 0 || (_flags & flags) != 0); + } + + /// + /// + /// + private static void OnShowGridLinesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Grid grid = (Grid)d; + + if ( grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + && grid.ListenToNotifications) + { + grid.InvalidateVisual(); + } + + grid.SetFlags((bool) e.NewValue, Flags.ShowGridLinesPropertyValue); + } + + /// + /// + /// + private static void OnCellAttachedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + Visual child = d as Visual; + + if (child != null) + { + Grid grid = VisualTreeHelper.GetParent(child) as Grid; + if ( grid != null + && grid.ExtData != null + && grid.ListenToNotifications ) + { + grid.CellsStructureDirty = true; + grid.InvalidateMeasure(); + } + } + } + + /// + /// + /// + private static bool IsIntValueNotNegative(object value) + { + return ((int)value >= 0); + } + + /// + /// + /// + private static bool IsIntValueGreaterThanZero(object value) + { + return ((int)value > 0); + } + + /// + /// Helper for Comparer methods. + /// + /// + /// true iff one or both of x and y are null, in which case result holds + /// the relative sort order. + /// + private static bool CompareNullRefs(object x, object y, out int result) + { + result = 2; + + if (x == null) + { + if (y == null) + { + result = 0; + } + else + { + result = -1; + } + } + else + { + if (y == null) + { + result = 1; + } + } + + return (result != 2); + } + + #endregion Private Methods + + //------------------------------------------------------ + // + // Private Properties + // + //------------------------------------------------------ + + #region Private Properties + + /// + /// Private version returning array of column definitions. + /// + private DefinitionBase[] DefinitionsU + { + get { return (ExtData.DefinitionsU); } + } + + /// + /// Private version returning array of row definitions. + /// + private DefinitionBase[] DefinitionsV + { + get { return (ExtData.DefinitionsV); } + } + + /// + /// Helper accessor to layout time array of definitions. + /// + private DefinitionBase[] TempDefinitions + { + get + { + ExtendedData extData = ExtData; + int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length) * 2; + + if ( extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength ) + { + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(s_tempDefinitionsDataSlot); + if (tempDefinitionsWeakRef == null) + { + extData.TempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(s_tempDefinitionsDataSlot, new WeakReference(extData.TempDefinitions)); + } + else + { + extData.TempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if ( extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength ) + { + extData.TempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = extData.TempDefinitions; + } + } + } + return (extData.TempDefinitions); + } + } + + /// + /// Helper accessor to definition indices. + /// + private int[] DefinitionIndices + { + get + { + int requiredLength = Math.Max(Math.Max(DefinitionsU.Length, DefinitionsV.Length), 1) * 2; + + if (_definitionIndices == null || _definitionIndices.Length < requiredLength) + { + _definitionIndices = new int[requiredLength]; + } + + return _definitionIndices; + } + } + + /// + /// Helper accessor to rounding errors. + /// + private double[] RoundingErrors + { + get + { + int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length); + + if (_roundingErrors == null && requiredLength == 0) + { + _roundingErrors = new double[1]; + } + else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) + { + _roundingErrors = new double[requiredLength]; + } + return _roundingErrors; + } + } + + /// + /// Private version returning array of cells. + /// + private CellCache[] PrivateCells + { + get { return (ExtData.CellCachesCollection); } + } + + /// + /// Convenience accessor to ValidCellsStructure bit flag. + /// + private bool CellsStructureDirty + { + get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } + set { SetFlags(!value, Flags.ValidCellsStructure); } + } + + /// + /// Convenience accessor to ListenToNotifications bit flag. + /// + private bool ListenToNotifications + { + get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } + set { SetFlags(value, Flags.ListenToNotifications); } + } + + /// + /// Convenience accessor to SizeToContentU bit flag. + /// + private bool SizeToContentU + { + get { return (CheckFlagsAnd(Flags.SizeToContentU)); } + set { SetFlags(value, Flags.SizeToContentU); } + } + + /// + /// Convenience accessor to SizeToContentV bit flag. + /// + private bool SizeToContentV + { + get { return (CheckFlagsAnd(Flags.SizeToContentV)); } + set { SetFlags(value, Flags.SizeToContentV); } + } + + /// + /// Convenience accessor to HasStarCellsU bit flag. + /// + private bool HasStarCellsU + { + get { return (CheckFlagsAnd(Flags.HasStarCellsU)); } + set { SetFlags(value, Flags.HasStarCellsU); } + } + + /// + /// Convenience accessor to HasStarCellsV bit flag. + /// + private bool HasStarCellsV + { + get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } + set { SetFlags(value, Flags.HasStarCellsV); } + } + + /// + /// Convenience accessor to HasGroup3CellsInAutoRows bit flag. + /// + private bool HasGroup3CellsInAutoRows + { + get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } + set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } + } + + /// + /// fp version of d == 0. + /// + /// Value to check. + /// true if d == 0. + private static bool _IsZero(double d) + { + return (Math.Abs(d) < c_epsilon); + } + + /// + /// fp version of d1 == d2 + /// + /// First value to compare + /// Second value to compare + /// true if d1 == d2 + private static bool _AreClose(double d1, double d2) + { + return (Math.Abs(d1 - d2) < c_epsilon); + } + + /// + /// Returns reference to extended data bag. + /// + private ExtendedData ExtData + { + get { return (_data); } + } + + /// + /// Returns *-weight, adjusted for scale computed during Phase 1 + /// + static double StarWeight(DefinitionBase def, double scale) + { + if (scale < 0.0) + { + // if one of the *-weights is Infinity, adjust the weights by mapping + // Infinty to 1.0 and everything else to 0.0: the infinite items share the + // available space equally, everyone else gets nothing. + return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; + } + else + { + return def.UserSize.Value * scale; + } + } + + #endregion Private Properties + + //------------------------------------------------------ + // + // Private Fields + // + //------------------------------------------------------ + + #region Private Fields + private ExtendedData _data; // extended data instantiated on demand, for non-trivial case handling only + private Flags _flags; // grid validity / property caches dirtiness flags + private GridLinesRenderer _gridLinesRenderer; + + // Keeps track of definition indices. + int[] _definitionIndices; + + // Stores unrounded values and rounding errors during layout rounding. + double[] _roundingErrors; + + #endregion Private Fields + + //------------------------------------------------------ + // + // Static Fields + // + //------------------------------------------------------ + + #region Static Fields + private const double c_epsilon = 1e-5; // used in fp calculations + private const double c_starClip = 1e298; // used as maximum for clipping star values during normalization + private const int c_layoutLoopMaxCount = 5; // 5 is an arbitrary constant chosen to end the measure loop + private static readonly LocalDataStoreSlot s_tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + private static readonly IComparer s_spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + private static readonly IComparer s_spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + private static readonly IComparer s_starDistributionOrderComparer = new StarDistributionOrderComparer(); + private static readonly IComparer s_distributionOrderComparer = new DistributionOrderComparer(); + private static readonly IComparer s_minRatioComparer = new MinRatioComparer(); + private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); + private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); + + #endregion Static Fields + + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ + + #region Private Structures Classes + + /// + /// Extended data instantiated on demand, when grid handles non-trivial case. + /// + private class ExtendedData + { + internal ColumnDefinitionCollection ColumnDefinitions; // collection of column definitions (logical tree support) + internal RowDefinitionCollection RowDefinitions; // collection of row definitions (logical tree support) + internal DefinitionBase[] DefinitionsU; // collection of column definitions used during calc + internal DefinitionBase[] DefinitionsV; // collection of row definitions used during calc + internal CellCache[] CellCachesCollection; // backing store for logical children + internal int CellGroup1; // index of the first cell in first cell group + internal int CellGroup2; // index of the first cell in second cell group + internal int CellGroup3; // index of the first cell in third cell group + internal int CellGroup4; // index of the first cell in forth cell group + internal DefinitionBase[] TempDefinitions; // temporary array used during layout for various purposes + // TempDefinitions.Length == Max(definitionsU.Length, definitionsV.Length) + } + + /// + /// Grid validity / property caches dirtiness flags + /// + [System.Flags] + private enum Flags + { + // + // the foolowing flags let grid tracking dirtiness in more granular manner: + // * Valid???Structure flags indicate that elements were added or removed. + // * Valid???Layout flags indicate that layout time portion of the information + // stored on the objects should be updated. + // + ValidDefinitionsUStructure = 0x00000001, + ValidDefinitionsVStructure = 0x00000002, + ValidCellsStructure = 0x00000004, + + // + // boolean properties state + // + ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? + + // + // boolean flags + // + ListenToNotifications = 0x00001000, // "0" when all notifications are ignored + SizeToContentU = 0x00002000, // "1" if calculating to content in U direction + SizeToContentV = 0x00004000, // "1" if calculating to content in V direction + HasStarCellsU = 0x00008000, // "1" if at least one cell belongs to a Star column + HasStarCellsV = 0x00010000, // "1" if at least one cell belongs to a Star row + HasGroup3CellsInAutoRows = 0x00020000, // "1" if at least one cell of group 3 belongs to an Auto row + MeasureOverrideInProgress = 0x00040000, // "1" while in the context of Grid.MeasureOverride + ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride + } + + #endregion Private Structures Classes + + //------------------------------------------------------ + // + // Properties + // + //------------------------------------------------------ + + #region Properties + + /// + /// ShowGridLines property. This property is used mostly + /// for simplification of visual debuggig. When it is set + /// to true grid lines are drawn to visualize location + /// of grid lines. + /// + public static readonly DependencyProperty ShowGridLinesProperty = + DependencyProperty.Register( + "ShowGridLines", + typeof(bool), + typeof(Grid), + new FrameworkPropertyMetadata( + false, + new PropertyChangedCallback(OnShowGridLinesPropertyChanged))); + + /// + /// Column property. This is an attached property. + /// Grid defines Column property, so that it can be set + /// on any element treated as a cell. Column property + /// specifies child's position with respect to columns. + /// + /// + /// Columns are 0 - based. In order to appear in first column, element + /// should have Column property set to 0. + /// Default value for the property is 0. + /// + [CommonDependencyProperty] + public static readonly DependencyProperty ColumnProperty = + DependencyProperty.RegisterAttached( + "Column", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 0, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueNotNegative)); + + /// + /// Row property. This is an attached property. + /// Grid defines Row, so that it can be set + /// on any element treated as a cell. Row property + /// specifies child's position with respect to rows. + /// + /// Rows are 0 - based. In order to appear in first row, element + /// should have Row property set to 0. + /// Default value for the property is 0. + /// + /// + [CommonDependencyProperty] + public static readonly DependencyProperty RowProperty = + DependencyProperty.RegisterAttached( + "Row", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 0, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueNotNegative)); + + /// + /// ColumnSpan property. This is an attached property. + /// Grid defines ColumnSpan, so that it can be set + /// on any element treated as a cell. ColumnSpan property + /// specifies child's width with respect to columns. + /// Example, ColumnSpan == 2 means that child will span across two columns. + /// + /// + /// Default value for the property is 1. + /// + [CommonDependencyProperty] + public static readonly DependencyProperty ColumnSpanProperty = + DependencyProperty.RegisterAttached( + "ColumnSpan", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 1, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueGreaterThanZero)); + + /// + /// RowSpan property. This is an attached property. + /// Grid defines RowSpan, so that it can be set + /// on any element treated as a cell. RowSpan property + /// specifies child's height with respect to row grid lines. + /// Example, RowSpan == 3 means that child will span across three rows. + /// + /// + /// Default value for the property is 1. + /// + [CommonDependencyProperty] + public static readonly DependencyProperty RowSpanProperty = + DependencyProperty.RegisterAttached( + "RowSpan", + typeof(int), + typeof(Grid), + new FrameworkPropertyMetadata( + 1, + new PropertyChangedCallback(OnCellAttachedPropertyChanged)), + new ValidateValueCallback(IsIntValueGreaterThanZero)); + + + /// + /// IsSharedSizeScope property marks scoping element for shared size. + /// + public static readonly DependencyProperty IsSharedSizeScopeProperty = + DependencyProperty.RegisterAttached( + "IsSharedSizeScope", + typeof(bool), + typeof(Grid), + new FrameworkPropertyMetadata( + false, + new PropertyChangedCallback(DefinitionBase.OnIsSharedSizeScopePropertyChanged))); + + #endregion Properties + + //------------------------------------------------------ + // + // Internal Structures / Classes + // + //------------------------------------------------------ + + #region Internal Structures Classes + + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } + + #endregion Internal Structures Classes + + //------------------------------------------------------ + // + // Private Structures / Classes + // + //------------------------------------------------------ + + #region Private Structures Classes + + /// + /// CellCache stored calculated values of + /// 1. attached cell positioning properties; + /// 2. size type; + /// 3. index of a next cell in the group; + /// + private struct CellCache + { + internal int ColumnIndex; + internal int RowIndex; + internal int ColumnSpan; + internal int RowSpan; + internal LayoutTimeSizeType SizeTypeU; + internal LayoutTimeSizeType SizeTypeV; + internal int Next; + internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } + internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } + } + + /// + /// Helper class for representing a key for a span in hashtable. + /// + private class SpanKey + { + /// + /// Constructor. + /// + /// Starting index of the span. + /// Span count. + /// true for columns; false for rows. + internal SpanKey(int start, int count, bool u) + { + _start = start; + _count = count; + _u = u; + } + + /// + /// + /// + public override int GetHashCode() + { + int hash = (_start ^ (_count << 2)); + + if (_u) hash &= 0x7ffffff; + else hash |= 0x8000000; + + return (hash); + } + + /// + /// + /// + public override bool Equals(object obj) + { + SpanKey sk = obj as SpanKey; + return ( sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u ); + } + + /// + /// Returns start index of the span. + /// + internal int Start { get { return (_start); } } + + /// + /// Returns span count. + /// + internal int Count { get { return (_count); } } + + /// + /// Returns true if this is a column span. + /// false if this is a row span. + /// + internal bool U { get { return (_u); } } + + private int _start; + private int _count; + private bool _u; + } + + /// + /// SpanPreferredDistributionOrderComparer. + /// + private class SpanPreferredDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.MinSize.CompareTo(definitionY.MinSize); + } + else + { + result = -1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = +1; + } + else + { + result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); + } + } + } + + return result; + } + } + + /// + /// SpanMaxDistributionOrderComparer. + /// + private class SpanMaxDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + else + { + result = +1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = -1; + } + else + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + } + } + + return result; + } + } + + /// + /// StarDistributionOrderComparer. + /// + private class StarDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// DistributionOrderComparer. + /// + private class DistributionOrderComparer: IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; + double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; + result = xprime.CompareTo(yprime); + } + + return result; + } + } + + + /// + /// StarDistributionOrderIndexComparer. + /// + private class StarDistributionOrderIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal StarDistributionOrderIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// DistributionOrderComparer. + /// + private class DistributionOrderIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal DistributionOrderIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; + double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; + result = xprime.CompareTo(yprime); + } + + return result; + } + } + + /// + /// RoundingErrorIndexComparer. + /// + private class RoundingErrorIndexComparer : IComparer + { + private readonly double[] errors; + + internal RoundingErrorIndexComparer(double[] errors) + { + Invariant.Assert(errors != null); + this.errors = errors; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + int result; + + if (!CompareNullRefs(indexX, indexY, out result)) + { + double errorX = errors[indexX.Value]; + double errorY = errors[indexY.Value]; + result = errorX.CompareTo(errorY); + } + + return result; + } + } + + /// + /// MinRatioComparer. + /// Sort by w/min (stored in MeasureSize), descending. + /// We query the list from the back, i.e. in ascending order of w/min. + /// + private class MinRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } + + /// + /// MaxRatioComparer. + /// Sort by w/max (stored in SizeCache), ascending. + /// We query the list from the back, i.e. in descending order of w/max. + /// + private class MaxRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// StarWeightComparer. + /// Sort by *-weight (stored in MeasureSize), ascending. + /// + private class StarWeightComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } + + /// + /// MinRatioIndexComparer. + /// + private class MinRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MinRatioIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } + + /// + /// MaxRatioIndexComparer. + /// + private class MaxRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MaxRatioIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// MaxRatioIndexComparer. + /// + private class StarWeightIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal StarWeightIndexComparer(DefinitionBase[] definitions) + { + Invariant.Assert(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } + + /// + /// Implementation of a simple enumerator of grid's logical children + /// + private class GridChildrenCollectionEnumeratorSimple : IEnumerator + { + internal GridChildrenCollectionEnumeratorSimple(Grid grid, bool includeChildren) + { + Debug.Assert(grid != null); + _currentEnumerator = -1; + _enumerator0 = new ColumnDefinitionCollection.Enumerator(grid.ExtData != null ? grid.ExtData.ColumnDefinitions : null); + _enumerator1 = new RowDefinitionCollection.Enumerator(grid.ExtData != null ? grid.ExtData.RowDefinitions : null); + // GridLineRenderer is NOT included into this enumerator. + _enumerator2Index = 0; + if (includeChildren) + { + _enumerator2Collection = grid.Children; + _enumerator2Count = _enumerator2Collection.Count; + } + else + { + _enumerator2Collection = null; + _enumerator2Count = 0; + } + } + + public bool MoveNext() + { + while (_currentEnumerator < 3) + { + if (_currentEnumerator >= 0) + { + switch (_currentEnumerator) + { + case (0): if (_enumerator0.MoveNext()) { _currentChild = _enumerator0.Current; return (true); } break; + case (1): if (_enumerator1.MoveNext()) { _currentChild = _enumerator1.Current; return (true); } break; + case (2): if (_enumerator2Index < _enumerator2Count) + { + _currentChild = _enumerator2Collection[_enumerator2Index]; + _enumerator2Index++; + return (true); + } + break; + } + } + _currentEnumerator++; + } + return (false); + } + + public Object Current + { + get + { + if (_currentEnumerator == -1) + { + #pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception + throw new InvalidOperationException(SR.Get(SRID.EnumeratorNotStarted)); + } + if (_currentEnumerator >= 3) + { + #pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception + throw new InvalidOperationException(SR.Get(SRID.EnumeratorReachedEnd)); + } + + // assert below is not true anymore since UIElementCollection allowes for null children + //Debug.Assert(_currentChild != null); + return (_currentChild); + } + } + + public void Reset() + { + _currentEnumerator = -1; + _currentChild = null; + _enumerator0.Reset(); + _enumerator1.Reset(); + _enumerator2Index = 0; + } + + private int _currentEnumerator; + private Object _currentChild; + private ColumnDefinitionCollection.Enumerator _enumerator0; + private RowDefinitionCollection.Enumerator _enumerator1; + private UIElementCollection _enumerator2Collection; + private int _enumerator2Index; + private int _enumerator2Count; + } + + /// + /// Helper to render grid lines. + /// + internal class GridLinesRenderer : DrawingVisual + { + /// + /// Static initialization + /// + static GridLinesRenderer() + { + s_oddDashPen = new Pen(Brushes.Blue, c_penWidth); + DoubleCollection oddDashArray = new DoubleCollection(); + oddDashArray.Add(c_dashLength); + oddDashArray.Add(c_dashLength); + s_oddDashPen.DashStyle = new DashStyle(oddDashArray, 0); + s_oddDashPen.DashCap = PenLineCap.Flat; + s_oddDashPen.Freeze(); + + s_evenDashPen = new Pen(Brushes.Yellow, c_penWidth); + DoubleCollection evenDashArray = new DoubleCollection(); + evenDashArray.Add(c_dashLength); + evenDashArray.Add(c_dashLength); + s_evenDashPen.DashStyle = new DashStyle(evenDashArray, c_dashLength); + s_evenDashPen.DashCap = PenLineCap.Flat; + s_evenDashPen.Freeze(); + } + + /// + /// UpdateRenderBounds. + /// + /// Size of render bounds + internal void UpdateRenderBounds(Size boundsSize) + { + using (DrawingContext drawingContext = RenderOpen()) + { + Grid grid = VisualTreeHelper.GetParent(this) as Grid; + if ( grid == null + || grid.ShowGridLines == false ) + { + return; + } + + for (int i = 1; i < grid.DefinitionsU.Length; ++i) + { + DrawGridLine( + drawingContext, + grid.DefinitionsU[i].FinalOffset, 0.0, + grid.DefinitionsU[i].FinalOffset, boundsSize.Height); + } + + for (int i = 1; i < grid.DefinitionsV.Length; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.DefinitionsV[i].FinalOffset, + boundsSize.Width, grid.DefinitionsV[i].FinalOffset); + } + } + } + + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) + { + Point start = new Point(startX, startY); + Point end = new Point(endX, endY); + drawingContext.DrawLine(s_oddDashPen, start, end); + drawingContext.DrawLine(s_evenDashPen, start, end); + } + + private const double c_dashLength = 4.0; // + private const double c_penWidth = 1.0; // + private static readonly Pen s_oddDashPen; // first pen to draw dash + private static readonly Pen s_evenDashPen; // second pen to draw dash + private static readonly Point c_zeroPoint = new Point(0, 0); + } + + #endregion Private Structures Classes + + //------------------------------------------------------ + // + // Extended debugging for grid + // + //------------------------------------------------------ + +#if GRIDPARANOIA + private static double _performanceFrequency; + private static readonly bool _performanceFrequencyInitialized = InitializePerformanceFrequency(); + + //CASRemoval:[System.Security.SuppressUnmanagedCodeSecurity, System.Runtime.InteropServices.DllImport("kernel32.dll")] + private static extern bool QueryPerformanceCounter(out long lpPerformanceCount); + + //CASRemoval:[System.Security.SuppressUnmanagedCodeSecurity, System.Runtime.InteropServices.DllImport("kernel32.dll")] + private static extern bool QueryPerformanceFrequency(out long lpFrequency); + + private static double CostInMilliseconds(long count) + { + return ((double)count / _performanceFrequency); + } + + private static long Cost(long startCount, long endCount) + { + long l = endCount - startCount; + if (l < 0) { l += long.MaxValue; } + return (l); + } + + private static bool InitializePerformanceFrequency() + { + long l; + QueryPerformanceFrequency(out l); + _performanceFrequency = (double)l * 0.001; + return (true); + } + + private struct Counter + { + internal long Start; + internal long Total; + internal int Calls; + } + + private Counter[] _counters; + private bool _hasNewCounterInfo; +#endif // GRIDPARANOIA + + // + // This property + // 1. Finds the correct initial size for the _effectiveValues store on the current DependencyObject + // 2. This is a performance optimization + // + internal override int EffectiveValuesInitialSize + { + get { return 9; } + } + + [Conditional("GRIDPARANOIA")] + internal void EnterCounterScope(Counters scopeCounter) + { + #if GRIDPARANOIA + if (ID == "CountThis") + { + if (_counters == null) + { + _counters = new Counter[(int)Counters.Count]; + } + ExitCounterScope(Counters.Default); + EnterCounter(scopeCounter); + } + else + { + _counters = null; + } + #endif // GRIDPARANOIA + } + + [Conditional("GRIDPARANOIA")] + internal void ExitCounterScope(Counters scopeCounter) + { + #if GRIDPARANOIA + if (_counters != null) + { + if (scopeCounter != Counters.Default) + { + ExitCounter(scopeCounter); + } + + if (_hasNewCounterInfo) + { + string NFormat = "F6"; + Console.WriteLine( + "\ncounter name | total t (ms) | # of calls | per call t (ms)" + + "\n----------------------+---------------+---------------+----------------------" ); + + for (int i = 0; i < _counters.Length; ++i) + { + if (_counters[i].Calls > 0) + { + Counters counter = (Counters)i; + double total = CostInMilliseconds(_counters[i].Total); + double single = total / _counters[i].Calls; + string counterName = counter.ToString(); + string separator; + + if (counterName.Length < 8) { separator = "\t\t\t"; } + else if (counterName.Length < 16) { separator = "\t\t"; } + else { separator = "\t"; } + + Console.WriteLine( + counter.ToString() + separator + + total.ToString(NFormat) + "\t" + + _counters[i].Calls + "\t\t" + + single.ToString(NFormat)); + + _counters[i] = new Counter(); + } + } + } + _hasNewCounterInfo = false; + } + #endif // GRIDPARANOIA + } + + [Conditional("GRIDPARANOIA")] + internal void EnterCounter(Counters counter) + { + #if GRIDPARANOIA + if (_counters != null) + { + Debug.Assert((int)counter < _counters.Length); + + int i = (int)counter; + QueryPerformanceCounter(out _counters[i].Start); + } + #endif // GRIDPARANOIA + } + + [Conditional("GRIDPARANOIA")] + internal void ExitCounter(Counters counter) + { + #if GRIDPARANOIA + if (_counters != null) + { + Debug.Assert((int)counter < _counters.Length); + + int i = (int)counter; + long l; + QueryPerformanceCounter(out l); + l = Cost(_counters[i].Start, l); + _counters[i].Total += l; + _counters[i].Calls++; + _hasNewCounterInfo = true; + } + #endif // GRIDPARANOIA + } + + internal enum Counters : int + { + Default = -1, + + MeasureOverride, + _ValidateColsStructure, + _ValidateRowsStructure, + _ValidateCells, + _MeasureCell, + __MeasureChild, + _CalculateDesiredSize, + + ArrangeOverride, + _SetFinalSize, + _ArrangeChildHelper2, + _PositionCell, + + Count, + } + } +} \ No newline at end of file From 7767319ce4c77178ef02cd30bcd2d376652979f9 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 23 May 2019 13:13:41 +0800 Subject: [PATCH 027/179] Porting part 1 of n --- src/Avalonia.Controls/GridWPF.cs | 593 +++++++++---------------------- 1 file changed, 168 insertions(+), 425 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 3a4b41314f..ff006a5c1b 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -10,96 +10,20 @@ using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; using Avalonia.VisualTree; +using System.Threading; using JetBrains.Annotations; - - -using MS.Internal; -using MS.Internal.Controls; -using MS.Internal.PresentationFramework; -using MS.Internal.Telemetry.PresentationFramework; -using MS.Utility; - -using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia; using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Windows.Threading; -using System.Threading; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Documents; -using System.Windows.Media; -using System.Windows.Markup; - -#pragma warning disable 1634, 1691 // suppressing PreSharp warnings namespace System.Windows.Controls { /// /// Grid /// - public class Grid : Panel, IAddChild + public class Grid : Panel { - //------------------------------------------------------ - // - // Constructors - // - //------------------------------------------------------ - - #region Constructors - - static Grid() - { - ControlsTraceLogger.AddControl(TelemetryControls.Grid); - } - - /// - /// Default constructor. - /// - public Grid() - { - SetFlags((bool) ShowGridLinesProperty.GetDefaultValue(DependencyObjectType), Flags.ShowGridLinesPropertyValue); - } - - #endregion Constructors - - //------------------------------------------------------ - // - // Public Methods - // - //------------------------------------------------------ - - #region Public Methods - - /// - /// - /// - void IAddChild.AddChild(object value) - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - UIElement cell = value as UIElement; - if (cell != null) - { - Children.Add(cell); - return; - } - - throw (new ArgumentException(SR.Get(SRID.Grid_UnexpectedParameterType, value.GetType(), typeof(UIElement)), "value")); - } - - /// - /// - /// - void IAddChild.AddText(string text) - { - XamlSerializerUtil.ThrowIfNonWhiteSpaceInAddText(text, this); - } - /// /// /// @@ -108,16 +32,16 @@ namespace System.Windows.Controls get { // empty panel or a panel being used as the items - // host has *no* logical children; give empty enumerator + // 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) ) + if (extData == null + || ((extData.ColumnDefinitions == null || extData.ColumnDefinitions.Count == 0) + && (extData.RowDefinitions == null || extData.RowDefinitions.Count == 0)) ) { // grid is empty @@ -130,11 +54,11 @@ namespace System.Windows.Controls } /// - /// Helper for setting Column property on a UIElement. + /// Helper for setting Column property on a Control. /// - /// UIElement to set Column property on. + /// Control to set Column property on. /// Column property value. - public static void SetColumn(UIElement element, int value) + public static void SetColumn(Control element, int value) { if (element == null) { @@ -145,12 +69,11 @@ namespace System.Windows.Controls } /// - /// Helper for reading Column property from a UIElement. + /// Helper for reading Column property from a Control. /// - /// UIElement to read Column property from. + /// Control to read Column property from. /// Column property value. - [AttachedPropertyBrowsableForChildren()] - public static int GetColumn(UIElement element) + public static int GetColumn(Control element) { if (element == null) { @@ -161,11 +84,11 @@ namespace System.Windows.Controls } /// - /// Helper for setting Row property on a UIElement. + /// Helper for setting Row property on a Control. /// - /// UIElement to set Row property on. + /// Control to set Row property on. /// Row property value. - public static void SetRow(UIElement element, int value) + public static void SetRow(Control element, int value) { if (element == null) { @@ -176,12 +99,12 @@ namespace System.Windows.Controls } /// - /// Helper for reading Row property from a UIElement. + /// Helper for reading Row property from a Control. /// - /// UIElement to read Row property from. + /// Control to read Row property from. /// Row property value. [AttachedPropertyBrowsableForChildren()] - public static int GetRow(UIElement element) + public static int GetRow(Control element) { if (element == null) { @@ -192,11 +115,11 @@ namespace System.Windows.Controls } /// - /// Helper for setting ColumnSpan property on a UIElement. + /// Helper for setting ColumnSpan property on a Control. /// - /// UIElement to set ColumnSpan property on. + /// Control to set ColumnSpan property on. /// ColumnSpan property value. - public static void SetColumnSpan(UIElement element, int value) + public static void SetColumnSpan(Control element, int value) { if (element == null) { @@ -207,12 +130,12 @@ namespace System.Windows.Controls } /// - /// Helper for reading ColumnSpan property from a UIElement. + /// Helper for reading ColumnSpan property from a Control. /// - /// UIElement to read ColumnSpan property from. + /// Control to read ColumnSpan property from. /// ColumnSpan property value. [AttachedPropertyBrowsableForChildren()] - public static int GetColumnSpan(UIElement element) + public static int GetColumnSpan(Control element) { if (element == null) { @@ -223,11 +146,11 @@ namespace System.Windows.Controls } /// - /// Helper for setting RowSpan property on a UIElement. + /// Helper for setting RowSpan property on a Control. /// - /// UIElement to set RowSpan property on. + /// Control to set RowSpan property on. /// RowSpan property value. - public static void SetRowSpan(UIElement element, int value) + public static void SetRowSpan(Control element, int value) { if (element == null) { @@ -238,12 +161,11 @@ namespace System.Windows.Controls } /// - /// Helper for reading RowSpan property from a UIElement. + /// Helper for reading RowSpan property from a Control. /// - /// UIElement to read RowSpan property from. + /// Control to read RowSpan property from. /// RowSpan property value. - [AttachedPropertyBrowsableForChildren()] - public static int GetRowSpan(UIElement element) + public static int GetRowSpan(Control element) { if (element == null) { @@ -254,11 +176,11 @@ namespace System.Windows.Controls } /// - /// Helper for setting IsSharedSizeScope property on a UIElement. + /// Helper for setting IsSharedSizeScope property on a Control. /// - /// UIElement to set IsSharedSizeScope property on. + /// Control to set IsSharedSizeScope property on. /// IsSharedSizeScope property value. - public static void SetIsSharedSizeScope(UIElement element, bool value) + public static void SetIsSharedSizeScope(Control element, bool value) { if (element == null) { @@ -269,11 +191,11 @@ namespace System.Windows.Controls } /// - /// Helper for reading IsSharedSizeScope property from a UIElement. + /// Helper for reading IsSharedSizeScope property from a Control. /// - /// UIElement to read IsSharedSizeScope property from. + /// Control to read IsSharedSizeScope property from. /// IsSharedSizeScope property value. - public static bool GetIsSharedSizeScope(UIElement element) + public static bool GetIsSharedSizeScope(Control element) { if (element == null) { @@ -283,7 +205,6 @@ namespace System.Windows.Controls return ((bool)element.GetValue(IsSharedSizeScopeProperty)); } - #endregion Public Methods //------------------------------------------------------ // @@ -343,10 +264,10 @@ namespace System.Windows.Controls #region Protected Methods /// - /// Derived class must implement to support Visual children. The method must return + /// 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. + /// By default a Visual does not have any Children. /// /// Remark: /// During this virtual call it is not valid to modify the Visual tree. @@ -355,7 +276,7 @@ namespace System.Windows.Controls { // because "base.Count + 1" for GridLinesRenderer // argument checking done at the base class - if(index == base.VisualChildrenCount) + if (index == base.VisualChildrenCount) { if (_gridLinesRenderer == null) { @@ -368,10 +289,10 @@ namespace System.Windows.Controls /// /// Derived classes override this property to enable the Visual code to enumerate - /// the Visual children. Derived classes need to return the number of children + /// the Visual Children. Derived classes need to return the number of Children /// from this method. /// - /// By default a Visual does not have any children. + /// By default a Visual does not have any Children. /// /// Remark: During this virtual method the Visual tree must not be modified. /// @@ -402,11 +323,10 @@ namespace System.Windows.Controls if (extData == null) { gridDesiredSize = new Size(); - UIElementCollection children = InternalChildren; - for (int i = 0, count = children.Count; i < count; ++i) + for (int i = 0, count = Children.Count; i < count; ++i) { - UIElement child = children[i]; + var child = Children[i]; if (child != null) { child.Measure(constraint); @@ -586,7 +506,7 @@ namespace System.Windows.Controls // +--------+ // // where: - // * all [Measure GroupN] - regular children measure process - + // * 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; @@ -638,7 +558,7 @@ namespace System.Windows.Controls // also use a count heuristic to break a loop in case of one. bool hasDesiredSizeUChanged = false; - int cnt=0; + int cnt = 0; // Cache Group2MinWidths & Group3MinHeights double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); @@ -700,11 +620,11 @@ namespace System.Windows.Controls if (_data == null) { - UIElementCollection children = InternalChildren; + ControlCollection Children = InternalChildren; - for (int i = 0, count = children.Count; i < count; ++i) + for (int i = 0, count = Children.Count; i < count; ++i) { - UIElement child = children[i]; + Control child = Children[i]; if (child != null) { child.Arrange(new Rect(arrangeSize)); @@ -722,11 +642,11 @@ namespace System.Windows.Controls ExitCounter(Counters._SetFinalSize); - UIElementCollection children = InternalChildren; + ControlCollection Children = InternalChildren; for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) { - UIElement cell = children[currentCell]; + Control cell = Children[currentCell]; if (cell == null) { continue; @@ -741,7 +661,7 @@ namespace System.Windows.Controls columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), - GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan) ); + GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); EnterCounter(Counters._ArrangeChildHelper2); cell.Arrange(cellRect); @@ -917,10 +837,10 @@ namespace System.Windows.Controls /// private void ValidateCellsCore() { - UIElementCollection children = InternalChildren; + ControlCollection Children = InternalChildren; ExtendedData extData = ExtData; - extData.CellCachesCollection = new CellCache[children.Count]; + extData.CellCachesCollection = new CellCache[Children.Count]; extData.CellGroup1 = int.MaxValue; extData.CellGroup2 = int.MaxValue; extData.CellGroup3 = int.MaxValue; @@ -932,7 +852,7 @@ namespace System.Windows.Controls for (int i = PrivateCells.Length - 1; i >= 0; --i) { - UIElement child = children[i]; + Control child = Children[i]; if (child == null) { continue; @@ -996,9 +916,9 @@ namespace System.Windows.Controls } else { - if ( cell.IsAutoU - // note below: if spans through Star column it is NOT Auto - && !cell.IsStarU ) + if (cell.IsAutoU + // note below: if spans through Star column it is NOT Auto + && !cell.IsStarU) { cell.Next = extData.CellGroup2; extData.CellGroup2 = i; @@ -1168,7 +1088,7 @@ namespace System.Windows.Controls { double[] minSizes = isRows ? new double[DefinitionsV.Length] : new double[DefinitionsU.Length]; - for (int j=0; j (double)o ) + if (o == null + || value > (double)o) { store[key] = value; } @@ -1355,8 +1274,8 @@ namespace System.Windows.Controls double cellMeasureWidth; double cellMeasureHeight; - if ( PrivateCells[cell].IsAutoU - && !PrivateCells[cell].IsStarU ) + 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" @@ -1376,8 +1295,8 @@ namespace System.Windows.Controls { cellMeasureHeight = double.PositiveInfinity; } - else if ( PrivateCells[cell].IsAutoV - && !PrivateCells[cell].IsStarV ) + 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" @@ -1393,7 +1312,7 @@ namespace System.Windows.Controls } EnterCounter(Counters.__MeasureChild); - UIElement child = InternalChildren[cell]; + Control child = InternalChildren[cell]; if (child != null) { Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); @@ -1508,12 +1427,12 @@ namespace System.Windows.Controls // 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 ); + Debug.Assert(minSize <= preferredSize + && preferredSize <= maxSize + && rangeMinSize <= rangePreferredSize + && rangePreferredSize <= rangeMaxSize); - if (maxMaxSize < maxSize) maxMaxSize = maxSize; + if (maxMaxSize < maxSize) maxMaxSize = maxSize; if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; tempDefinitions[i - start] = definitions[i]; } @@ -1607,8 +1526,8 @@ namespace System.Windows.Controls // double equalSize = requestedSize / count; - if ( equalSize < maxMaxSize - && !_AreClose(equalSize, maxMaxSize) ) + 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 @@ -1617,12 +1536,12 @@ namespace System.Windows.Controls double sizeToDistribute = requestedSize - rangeMaxSize; // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers - Debug.Assert( !double.IsInfinity(totalRemainingSize) - && !DoubleUtil.IsNaN(totalRemainingSize) - && totalRemainingSize > 0 - && !double.IsInfinity(sizeToDistribute) - && !DoubleUtil.IsNaN(sizeToDistribute) - && sizeToDistribute > 0 ); + Debug.Assert(!double.IsInfinity(totalRemainingSize) + && !DoubleUtil.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !DoubleUtil.IsNaN(sizeToDistribute) + && sizeToDistribute > 0); for (int i = 0; i < count; ++i) { @@ -1706,9 +1625,9 @@ namespace System.Windows.Controls // 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; + double maxSize = Math.Max(definitions[i].MinSize, definitions[i].UserMaxSize); + maxSize = Math.Min(maxSize, c_starClip); + definitions[i].SizeCache = maxSize / starValue; } } break; @@ -1746,12 +1665,12 @@ namespace System.Windows.Controls 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); + resolvedSize = Math.Min(userSize, tempDefinitions[i].UserMaxSize); + resolvedSize = Math.Max(tempDefinitions[i].MinSize, resolvedSize); } tempDefinitions[i].MeasureSize = resolvedSize; - takenSize += resolvedSize; + takenSize += resolvedSize; } while (++i < starDefinitionsCount); } } @@ -1783,7 +1702,7 @@ namespace System.Windows.Controls // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights double maxStar = 0.0; - for (int i=0; i finalSize - && !_AreClose(allPreferredArrangeSize, finalSize) ) + if (allPreferredArrangeSize > finalSize + && !_AreClose(allPreferredArrangeSize, finalSize)) { DistributionOrderIndexComparer distributionOrderIndexComparer = new DistributionOrderIndexComparer(definitions); Array.Sort(definitionIndices, 0, definitions.Length, distributionOrderIndexComparer); @@ -2267,7 +2186,7 @@ namespace System.Windows.Controls if (useLayoutRounding) { roundingErrors[definitionIndex] = final; - final = UIElement.RoundLayoutValue(finalOld, dpi); + final = Control.RoundLayoutValue(finalOld, dpi); final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); final = Math.Min(final, definitions[definitionIndex].SizeCache); } @@ -2294,7 +2213,7 @@ namespace System.Windows.Controls RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); double adjustedSize = allPreferredArrangeSize; - double dpiIncrement = UIElement.RoundLayoutValue(1.0, dpi); + double dpiIncrement = Control.RoundLayoutValue(1.0, dpi); if (allPreferredArrangeSize > finalSize) { @@ -2365,7 +2284,7 @@ namespace System.Windows.Controls // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights double maxStar = 0.0; - for (int i=0; i finalSize) { @@ -2895,8 +2814,8 @@ namespace System.Windows.Controls 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 (); + // for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); + // for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); if (extData.TempDefinitions != null) { @@ -2914,9 +2833,9 @@ namespace System.Windows.Controls public bool ShouldSerializeColumnDefinitions() { ExtendedData extData = ExtData; - return ( extData != null - && extData.ColumnDefinitions != null - && extData.ColumnDefinitions.Count > 0 ); + return (extData != null + && extData.ColumnDefinitions != null + && extData.ColumnDefinitions.Count > 0); } /// @@ -2926,9 +2845,9 @@ namespace System.Windows.Controls public bool ShouldSerializeRowDefinitions() { ExtendedData extData = ExtData; - return ( extData != null - && extData.RowDefinitions != null - && extData.RowDefinitions.Count > 0 ); + return (extData != null + && extData.RowDefinitions != null + && extData.RowDefinitions.Count > 0); } /// @@ -2994,13 +2913,13 @@ namespace System.Windows.Controls { Grid grid = (Grid)d; - if ( grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway - && grid.ListenToNotifications) + 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); + grid.SetFlags((bool)e.NewValue, Flags.ShowGridLinesPropertyValue); } /// @@ -3013,9 +2932,9 @@ namespace System.Windows.Controls if (child != null) { Grid grid = VisualTreeHelper.GetParent(child) as Grid; - if ( grid != null - && grid.ExtData != null - && grid.ListenToNotifications ) + if (grid != null + && grid.ExtData != null + && grid.ListenToNotifications) { grid.CellsStructureDirty = true; grid.InvalidateMeasure(); @@ -3108,8 +3027,8 @@ namespace System.Windows.Controls ExtendedData extData = ExtData; int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length) * 2; - if ( extData.TempDefinitions == null - || extData.TempDefinitions.Length < requiredLength ) + if (extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength) { WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(s_tempDefinitionsDataSlot); if (tempDefinitionsWeakRef == null) @@ -3120,8 +3039,8 @@ namespace System.Windows.Controls else { extData.TempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; - if ( extData.TempDefinitions == null - || extData.TempDefinitions.Length < requiredLength ) + if (extData.TempDefinitions == null + || extData.TempDefinitions.Length < requiredLength) { extData.TempDefinitions = new DefinitionBase[requiredLength]; tempDefinitionsWeakRef.Target = extData.TempDefinitions; @@ -3348,7 +3267,7 @@ namespace System.Windows.Controls internal RowDefinitionCollection RowDefinitions; // collection of row definitions (logical tree support) internal DefinitionBase[] DefinitionsU; // collection of column definitions used during calc internal DefinitionBase[] DefinitionsV; // collection of row definitions used during calc - internal CellCache[] CellCachesCollection; // backing store for logical children + internal 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 @@ -3369,26 +3288,26 @@ namespace System.Windows.Controls // * Valid???Layout flags indicate that layout time portion of the information // stored on the objects should be updated. // - ValidDefinitionsUStructure = 0x00000001, - ValidDefinitionsVStructure = 0x00000002, - ValidCellsStructure = 0x00000004, + ValidDefinitionsUStructure = 0x00000001, + ValidDefinitionsVStructure = 0x00000002, + ValidCellsStructure = 0x00000004, // // boolean properties state // - ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? + 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 + ListenToNotifications = 0x00001000, // "0" when all notifications are ignored + SizeToContentU = 0x00002000, // "1" if calculating to content in U direction + SizeToContentV = 0x00004000, // "1" if calculating to content in V direction + HasStarCellsU = 0x00008000, // "1" if at least one cell belongs to a Star column + HasStarCellsV = 0x00010000, // "1" if at least one cell belongs to a Star row + HasGroup3CellsInAutoRows = 0x00020000, // "1" if at least one cell of group 3 belongs to an Auto row + MeasureOverrideInProgress = 0x00040000, // "1" while in the context of Grid.MeasureOverride + ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride } #endregion Private Structures Classes @@ -3506,7 +3425,7 @@ namespace System.Windows.Controls /// /// IsSharedSizeScope property marks scoping element for shared size. /// - public static readonly DependencyProperty IsSharedSizeScopeProperty = + public static readonly DependencyProperty IsSharedSizeScopeProperty = DependencyProperty.RegisterAttached( "IsSharedSizeScope", typeof(bool), @@ -3531,10 +3450,10 @@ namespace System.Windows.Controls [System.Flags] internal enum LayoutTimeSizeType : byte { - None = 0x00, - Pixel = 0x01, - Auto = 0x02, - Star = 0x04, + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, } #endregion Internal Structures Classes @@ -3594,7 +3513,7 @@ namespace System.Windows.Controls int hash = (_start ^ (_count << 2)); if (_u) hash &= 0x7ffffff; - else hash |= 0x8000000; + else hash |= 0x8000000; return (hash); } @@ -3605,10 +3524,10 @@ namespace System.Windows.Controls public override bool Equals(object obj) { SpanKey sk = obj as SpanKey; - return ( sk != null - && sk._start == _start - && sk._count == _count - && sk._u == _u ); + return (sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u); } /// @@ -3740,7 +3659,7 @@ namespace System.Windows.Controls /// /// DistributionOrderComparer. /// - private class DistributionOrderComparer: IComparer + private class DistributionOrderComparer : IComparer { public int Compare(object x, object y) { @@ -4068,7 +3987,7 @@ namespace System.Windows.Controls } /// - /// Implementation of a simple enumerator of grid's logical children + /// Implementation of a simple enumerator of grid's logical Children /// private class GridChildrenCollectionEnumeratorSimple : IEnumerator { @@ -4102,13 +4021,14 @@ namespace System.Windows.Controls { 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; + case (2): + if (_enumerator2Index < _enumerator2Count) + { + _currentChild = _enumerator2Collection[_enumerator2Index]; + _enumerator2Index++; + return (true); + } + break; } } _currentEnumerator++; @@ -4122,16 +4042,16 @@ namespace System.Windows.Controls { if (_currentEnumerator == -1) { - #pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception +#pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception throw new InvalidOperationException(SR.Get(SRID.EnumeratorNotStarted)); } if (_currentEnumerator >= 3) { - #pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception +#pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception throw new InvalidOperationException(SR.Get(SRID.EnumeratorReachedEnd)); } - // assert below is not true anymore since UIElementCollection allowes for null children + // assert below is not true anymore since ControlCollection allowes for null Children //Debug.Assert(_currentChild != null); return (_currentChild); } @@ -4150,7 +4070,7 @@ namespace System.Windows.Controls private Object _currentChild; private ColumnDefinitionCollection.Enumerator _enumerator0; private RowDefinitionCollection.Enumerator _enumerator1; - private UIElementCollection _enumerator2Collection; + private ControlCollection _enumerator2Collection; private int _enumerator2Index; private int _enumerator2Count; } @@ -4191,8 +4111,8 @@ namespace System.Windows.Controls using (DrawingContext drawingContext = RenderOpen()) { Grid grid = VisualTreeHelper.GetParent(this) as Grid; - if ( grid == null - || grid.ShowGridLines == false ) + if (grid == null + || grid.ShowGridLines == false) { return; } @@ -4239,182 +4159,5 @@ namespace System.Windows.Controls } #endregion Private Structures Classes - - //------------------------------------------------------ - // - // Extended debugging for grid - // - //------------------------------------------------------ - -#if GRIDPARANOIA - private static double _performanceFrequency; - private static readonly bool _performanceFrequencyInitialized = InitializePerformanceFrequency(); - - //CASRemoval:[System.Security.SuppressUnmanagedCodeSecurity, System.Runtime.InteropServices.DllImport("kernel32.dll")] - private static extern bool QueryPerformanceCounter(out long lpPerformanceCount); - - //CASRemoval:[System.Security.SuppressUnmanagedCodeSecurity, System.Runtime.InteropServices.DllImport("kernel32.dll")] - private static extern bool QueryPerformanceFrequency(out long lpFrequency); - - private static double CostInMilliseconds(long count) - { - return ((double)count / _performanceFrequency); - } - - private static long Cost(long startCount, long endCount) - { - long l = endCount - startCount; - if (l < 0) { l += long.MaxValue; } - return (l); - } - - private static bool InitializePerformanceFrequency() - { - long l; - QueryPerformanceFrequency(out l); - _performanceFrequency = (double)l * 0.001; - return (true); - } - - private struct Counter - { - internal long Start; - internal long Total; - internal int Calls; - } - - private Counter[] _counters; - private bool _hasNewCounterInfo; -#endif // GRIDPARANOIA - - // - // This property - // 1. Finds the correct initial size for the _effectiveValues store on the current DependencyObject - // 2. This is a performance optimization - // - internal override int EffectiveValuesInitialSize - { - get { return 9; } - } - - [Conditional("GRIDPARANOIA")] - internal void EnterCounterScope(Counters scopeCounter) - { - #if GRIDPARANOIA - if (ID == "CountThis") - { - if (_counters == null) - { - _counters = new Counter[(int)Counters.Count]; - } - ExitCounterScope(Counters.Default); - EnterCounter(scopeCounter); - } - else - { - _counters = null; - } - #endif // GRIDPARANOIA - } - - [Conditional("GRIDPARANOIA")] - internal void ExitCounterScope(Counters scopeCounter) - { - #if GRIDPARANOIA - if (_counters != null) - { - if (scopeCounter != Counters.Default) - { - ExitCounter(scopeCounter); - } - - if (_hasNewCounterInfo) - { - string NFormat = "F6"; - Console.WriteLine( - "\ncounter name | total t (ms) | # of calls | per call t (ms)" - + "\n----------------------+---------------+---------------+----------------------" ); - - for (int i = 0; i < _counters.Length; ++i) - { - if (_counters[i].Calls > 0) - { - Counters counter = (Counters)i; - double total = CostInMilliseconds(_counters[i].Total); - double single = total / _counters[i].Calls; - string counterName = counter.ToString(); - string separator; - - if (counterName.Length < 8) { separator = "\t\t\t"; } - else if (counterName.Length < 16) { separator = "\t\t"; } - else { separator = "\t"; } - - Console.WriteLine( - counter.ToString() + separator - + total.ToString(NFormat) + "\t" - + _counters[i].Calls + "\t\t" - + single.ToString(NFormat)); - - _counters[i] = new Counter(); - } - } - } - _hasNewCounterInfo = false; - } - #endif // GRIDPARANOIA - } - - [Conditional("GRIDPARANOIA")] - internal void EnterCounter(Counters counter) - { - #if GRIDPARANOIA - if (_counters != null) - { - Debug.Assert((int)counter < _counters.Length); - - int i = (int)counter; - QueryPerformanceCounter(out _counters[i].Start); - } - #endif // GRIDPARANOIA - } - - [Conditional("GRIDPARANOIA")] - internal void ExitCounter(Counters counter) - { - #if GRIDPARANOIA - if (_counters != null) - { - Debug.Assert((int)counter < _counters.Length); - - int i = (int)counter; - long l; - QueryPerformanceCounter(out l); - l = Cost(_counters[i].Start, l); - _counters[i].Total += l; - _counters[i].Calls++; - _hasNewCounterInfo = true; - } - #endif // GRIDPARANOIA - } - - internal enum Counters : int - { - Default = -1, - - MeasureOverride, - _ValidateColsStructure, - _ValidateRowsStructure, - _ValidateCells, - _MeasureCell, - __MeasureChild, - _CalculateDesiredSize, - - ArrangeOverride, - _SetFinalSize, - _ArrangeChildHelper2, - _PositionCell, - - Count, - } } } \ No newline at end of file From 90e6db45aaf8c4c4dc7333d1ba1ee8a2d924957f Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 23 May 2019 13:38:13 +0800 Subject: [PATCH 028/179] part 2 of n --- src/Avalonia.Controls/Grid.cs | 1200 +++++++++++++++--------------- src/Avalonia.Controls/GridWPF.cs | 394 ++-------- 2 files changed, 670 insertions(+), 924 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 90a27d0b31..98cd5aad02 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1,601 +1,601 @@ -// 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.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.VisualTree; -using JetBrains.Annotations; - -namespace Avalonia.Controls -{ - /// - /// Lays out child controls according to a grid. - /// - public class Grid : Panel - { - /// - /// Defines the Column attached property. - /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached( - "Column", - validate: ValidateColumn); - - /// - /// Defines the ColumnSpan attached property. - /// - public static readonly AttachedProperty ColumnSpanProperty = - AvaloniaProperty.RegisterAttached("ColumnSpan", 1); - - /// - /// Defines the Row attached property. - /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached( - "Row", - validate: ValidateRow); - - /// - /// Defines the RowSpan attached property. - /// - public static readonly AttachedProperty RowSpanProperty = - AvaloniaProperty.RegisterAttached("RowSpan", 1); - - public static readonly AttachedProperty IsSharedSizeScopeProperty = - AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - - protected override void OnMeasureInvalidated() - { - base.OnMeasureInvalidated(); - _sharedSizeHost?.InvalidateMeasure(this); - } - - private SharedSizeScopeHost _sharedSizeHost; - - /// - /// Defines the SharedSizeScopeHost private property. - /// The ampersands are used to make accessing the property via xaml inconvenient. - /// - internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); - - private ColumnDefinitions _columnDefinitions; - - private RowDefinitions _rowDefinitions; - - static Grid() - { - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); - } - - public Grid() - { - this.AttachedToVisualTree += Grid_AttachedToVisualTree; - this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; - } - - /// - /// Gets or sets the columns definitions for the grid. - /// - public ColumnDefinitions ColumnDefinitions - { - get - { - if (_columnDefinitions == null) - { - ColumnDefinitions = new ColumnDefinitions(); - } - - return _columnDefinitions; - } - - set - { - if (_columnDefinitions != null) - { - throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); - } - - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); - } - } - - /// - /// Gets or sets the row definitions for the grid. - /// - public RowDefinitions RowDefinitions - { - get - { - if (_rowDefinitions == null) - { - RowDefinitions = new RowDefinitions(); - } - - return _rowDefinitions; - } - - set - { - if (_rowDefinitions != null) - { - throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); - } - - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); - } - } - - /// - /// Gets the value of the Column attached property for a control. - /// - /// The control. - /// The control's column. - public static int GetColumn(AvaloniaObject element) - { - return element.GetValue(ColumnProperty); - } - - /// - /// Gets the value of the ColumnSpan attached property for a control. - /// - /// The control. - /// The control's column span. - public static int GetColumnSpan(AvaloniaObject element) - { - return element.GetValue(ColumnSpanProperty); - } - - /// - /// Gets the value of the Row attached property for a control. - /// - /// The control. - /// The control's row. - public static int GetRow(AvaloniaObject element) - { - return element.GetValue(RowProperty); - } - - /// - /// Gets the value of the RowSpan attached property for a control. - /// - /// The control. - /// The control's row span. - public static int GetRowSpan(AvaloniaObject element) - { - return element.GetValue(RowSpanProperty); - } - - - /// - /// Gets the value of the IsSharedSizeScope attached property for a control. - /// - /// The control. - /// The control's IsSharedSizeScope value. - public static bool GetIsSharedSizeScope(AvaloniaObject element) - { - return element.GetValue(IsSharedSizeScopeProperty); - } - - /// - /// Sets the value of the Column attached property for a control. - /// - /// The control. - /// The column value. - public static void SetColumn(AvaloniaObject element, int value) - { - element.SetValue(ColumnProperty, value); - } - - /// - /// Sets the value of the ColumnSpan attached property for a control. - /// - /// The control. - /// The column span value. - public static void SetColumnSpan(AvaloniaObject element, int value) - { - element.SetValue(ColumnSpanProperty, value); - } - - /// - /// Sets the value of the Row attached property for a control. - /// - /// The control. - /// The row value. - public static void SetRow(AvaloniaObject element, int value) - { - element.SetValue(RowProperty, value); - } - - /// - /// Sets the value of the RowSpan attached property for a control. - /// - /// The control. - /// The row span value. - public static void SetRowSpan(AvaloniaObject element, int value) - { - element.SetValue(RowSpanProperty, value); - } - - /// - /// Sets the value of IsSharedSizeScope property for a control. - /// - /// The control. - /// The IsSharedSizeScope value. - public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) - { - element.SetValue(IsSharedSizeScopeProperty, value); - } - - /// - /// Gets the result of the last column measurement. - /// Use this result to reduce the arrange calculation. - /// - private GridLayout.MeasureResult _columnMeasureCache; - - /// - /// Gets the result of the last row measurement. - /// Use this result to reduce the arrange calculation. - /// - private GridLayout.MeasureResult _rowMeasureCache; - - /// - /// Gets the row layout as of the last measure. - /// - private GridLayout _rowLayoutCache; - - /// - /// Gets the column layout as of the last measure. - /// - private GridLayout _columnLayoutCache; - - /// - /// Measures the grid. - /// - /// 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. - - 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); - } - - maxWidth = Math.Min(maxWidth, constraint.Width); - maxHeight = Math.Min(maxHeight, constraint.Height); - return new Size(maxWidth, maxHeight); - } - - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. - - var measureCache = new Dictionary(); - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - // Note: If a child stays in a * or Auto column/row, use constraint to measure it. - columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); - rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); - - // Calculate measurement. - var columnResult = columnLayout.Measure(constraint.Width); - var rowResult = rowLayout.Measure(constraint.Height); - - // Use the results of the measurement to measure the rest of the children. - foreach (var child in Children.OfType()) - { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); - var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); - - MeasureOnce(child, new Size(width, height)); - } - - // Cache the measure result and return the desired size. - _columnMeasureCache = columnResult; - _rowMeasureCache = rowResult; - _rowLayoutCache = rowLayout; - _columnLayoutCache = columnLayout; - - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { - _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); - } - - return new Size(columnResult.DesiredLength, rowResult.DesiredLength); - - // Measure each child only once. - // If a child has been measured, it will just return the desired size. - Size MeasureOnce(Control child, Size size) - { - if (measureCache.TryGetValue(child, out var desiredSize)) - { - return desiredSize; - } - - child.Measure(size); - desiredSize = child.DesiredSize; - measureCache[child] = desiredSize; - return desiredSize; - } - } - - /// - /// Arranges the grid's children. - /// - /// The size allocated to the control. - /// The space taken. - protected override Size ArrangeOverride(Size finalSize) - { - // 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) - { - foreach (var child in Children.OfType()) - { - child.Arrange(new Rect(finalSize)); - } - - return finalSize; - } - - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. - - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = _columnLayoutCache; - var rowLayout = _rowLayoutCache; - - var rowCache = _rowMeasureCache; - var columnCache = _columnMeasureCache; - - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { - (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); +// // 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.Generic; +// using System.Diagnostics; +// using System.Linq; +// using System.Reactive.Linq; +// using System.Runtime.CompilerServices; +// using Avalonia.Collections; +// using Avalonia.Controls.Utils; +// using Avalonia.VisualTree; +// using JetBrains.Annotations; + +// namespace Avalonia.Controls +// { +// /// +// /// Lays out child controls according to a grid. +// /// +// public class Grid : Panel +// { +// /// +// /// Defines the Column attached property. +// /// +// public static readonly AttachedProperty ColumnProperty = +// AvaloniaProperty.RegisterAttached( +// "Column", +// validate: ValidateColumn); + +// /// +// /// Defines the ColumnSpan attached property. +// /// +// public static readonly AttachedProperty ColumnSpanProperty = +// AvaloniaProperty.RegisterAttached("ColumnSpan", 1); + +// /// +// /// Defines the Row attached property. +// /// +// public static readonly AttachedProperty RowProperty = +// AvaloniaProperty.RegisterAttached( +// "Row", +// validate: ValidateRow); + +// /// +// /// Defines the RowSpan attached property. +// /// +// public static readonly AttachedProperty RowSpanProperty = +// AvaloniaProperty.RegisterAttached("RowSpan", 1); + +// public static readonly AttachedProperty IsSharedSizeScopeProperty = +// AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + +// protected override void OnMeasureInvalidated() +// { +// base.OnMeasureInvalidated(); +// _sharedSizeHost?.InvalidateMeasure(this); +// } + +// private SharedSizeScopeHost _sharedSizeHost; + +// /// +// /// Defines the SharedSizeScopeHost private property. +// /// The ampersands are used to make accessing the property via xaml inconvenient. +// /// +// internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = +// AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); + +// private ColumnDefinitions _columnDefinitions; + +// private RowDefinitions _rowDefinitions; + +// static Grid() +// { +// AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); +// IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); +// } + +// public Grid() +// { +// this.AttachedToVisualTree += Grid_AttachedToVisualTree; +// this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; +// } + +// /// +// /// Gets or sets the columns definitions for the grid. +// /// +// public ColumnDefinitions ColumnDefinitions +// { +// get +// { +// if (_columnDefinitions == null) +// { +// ColumnDefinitions = new ColumnDefinitions(); +// } + +// return _columnDefinitions; +// } + +// set +// { +// if (_columnDefinitions != null) +// { +// throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); +// } + +// _columnDefinitions = value; +// _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); +// _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); +// } +// } + +// /// +// /// Gets or sets the row definitions for the grid. +// /// +// public RowDefinitions RowDefinitions +// { +// get +// { +// if (_rowDefinitions == null) +// { +// RowDefinitions = new RowDefinitions(); +// } + +// return _rowDefinitions; +// } + +// set +// { +// if (_rowDefinitions != null) +// { +// throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); +// } + +// _rowDefinitions = value; +// _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); +// _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); +// } +// } + +// /// +// /// Gets the value of the Column attached property for a control. +// /// +// /// The control. +// /// The control's column. +// public static int GetColumn(AvaloniaObject element) +// { +// return element.GetValue(ColumnProperty); +// } + +// /// +// /// Gets the value of the ColumnSpan attached property for a control. +// /// +// /// The control. +// /// The control's column span. +// public static int GetColumnSpan(AvaloniaObject element) +// { +// return element.GetValue(ColumnSpanProperty); +// } + +// /// +// /// Gets the value of the Row attached property for a control. +// /// +// /// The control. +// /// The control's row. +// public static int GetRow(AvaloniaObject element) +// { +// return element.GetValue(RowProperty); +// } + +// /// +// /// Gets the value of the RowSpan attached property for a control. +// /// +// /// The control. +// /// The control's row span. +// public static int GetRowSpan(AvaloniaObject element) +// { +// return element.GetValue(RowSpanProperty); +// } + + +// /// +// /// Gets the value of the IsSharedSizeScope attached property for a control. +// /// +// /// The control. +// /// The control's IsSharedSizeScope value. +// public static bool GetIsSharedSizeScope(AvaloniaObject element) +// { +// return element.GetValue(IsSharedSizeScopeProperty); +// } + +// /// +// /// Sets the value of the Column attached property for a control. +// /// +// /// The control. +// /// The column value. +// public static void SetColumn(AvaloniaObject element, int value) +// { +// element.SetValue(ColumnProperty, value); +// } + +// /// +// /// Sets the value of the ColumnSpan attached property for a control. +// /// +// /// The control. +// /// The column span value. +// public static void SetColumnSpan(AvaloniaObject element, int value) +// { +// element.SetValue(ColumnSpanProperty, value); +// } + +// /// +// /// Sets the value of the Row attached property for a control. +// /// +// /// The control. +// /// The row value. +// public static void SetRow(AvaloniaObject element, int value) +// { +// element.SetValue(RowProperty, value); +// } + +// /// +// /// Sets the value of the RowSpan attached property for a control. +// /// +// /// The control. +// /// The row span value. +// public static void SetRowSpan(AvaloniaObject element, int value) +// { +// element.SetValue(RowSpanProperty, value); +// } + +// /// +// /// Sets the value of IsSharedSizeScope property for a control. +// /// +// /// The control. +// /// The IsSharedSizeScope value. +// public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) +// { +// element.SetValue(IsSharedSizeScopeProperty, value); +// } + +// /// +// /// Gets the result of the last column measurement. +// /// Use this result to reduce the arrange calculation. +// /// +// private GridLayout.MeasureResult _columnMeasureCache; + +// /// +// /// Gets the result of the last row measurement. +// /// Use this result to reduce the arrange calculation. +// /// +// private GridLayout.MeasureResult _rowMeasureCache; + +// /// +// /// Gets the row layout as of the last measure. +// /// +// private GridLayout _rowLayoutCache; + +// /// +// /// Gets the column layout as of the last measure. +// /// +// private GridLayout _columnLayoutCache; + +// /// +// /// Measures the grid. +// /// +// /// 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. + +// 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); +// } + +// maxWidth = Math.Min(maxWidth, constraint.Width); +// maxHeight = Math.Min(maxHeight, constraint.Height); +// return new Size(maxWidth, maxHeight); +// } + +// // Situation 2/2: +// // If the grid defines some columns or rows. +// // Debug Tip: +// // - GridLayout doesn't hold any state, so you can drag the debugger execution +// // arrow back to any statements and re-run them without any side-effect. + +// var measureCache = new Dictionary(); +// var (safeColumns, safeRows) = GetSafeColumnRows(); +// var columnLayout = new GridLayout(ColumnDefinitions); +// var rowLayout = new GridLayout(RowDefinitions); +// // Note: If a child stays in a * or Auto column/row, use constraint to measure it. +// columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); +// rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); + +// // Calculate measurement. +// var columnResult = columnLayout.Measure(constraint.Width); +// var rowResult = rowLayout.Measure(constraint.Height); + +// // Use the results of the measurement to measure the rest of the children. +// foreach (var child in Children.OfType()) +// { +// var (column, columnSpan) = safeColumns[child]; +// var (row, rowSpan) = safeRows[child]; +// var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); +// var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); + +// MeasureOnce(child, new Size(width, height)); +// } + +// // Cache the measure result and return the desired size. +// _columnMeasureCache = columnResult; +// _rowMeasureCache = rowResult; +// _rowLayoutCache = rowLayout; +// _columnLayoutCache = columnLayout; + +// if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) +// { +// _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); +// } + +// return new Size(columnResult.DesiredLength, rowResult.DesiredLength); + +// // Measure each child only once. +// // If a child has been measured, it will just return the desired size. +// Size MeasureOnce(Control child, Size size) +// { +// if (measureCache.TryGetValue(child, out var desiredSize)) +// { +// return desiredSize; +// } + +// child.Measure(size); +// desiredSize = child.DesiredSize; +// measureCache[child] = desiredSize; +// return desiredSize; +// } +// } + +// /// +// /// Arranges the grid's children. +// /// +// /// The size allocated to the control. +// /// The space taken. +// protected override Size ArrangeOverride(Size finalSize) +// { +// // 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) +// { +// foreach (var child in Children.OfType()) +// { +// child.Arrange(new Rect(finalSize)); +// } + +// return finalSize; +// } + +// // Situation 2/2: +// // If the grid defines some columns or rows. +// // Debug Tip: +// // - GridLayout doesn't hold any state, so you can drag the debugger execution +// // arrow back to any statements and re-run them without any side-effect. + +// var (safeColumns, safeRows) = GetSafeColumnRows(); +// var columnLayout = _columnLayoutCache; +// var rowLayout = _rowLayoutCache; + +// var rowCache = _rowMeasureCache; +// var columnCache = _columnMeasureCache; + +// if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) +// { +// (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); - rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); - columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); - } - - // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); - var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); - // Arrange the children. - foreach (var child in Children.OfType()) - { - 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)); - } - - // Assign the actual width. - for (var i = 0; i < ColumnDefinitions.Count; i++) - { - ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; - } - - // Assign the actual height. - for (var i = 0; i < RowDefinitions.Count; i++) - { - RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; - } - - // Return the render size. - return finalSize; - } - - /// - /// Tests whether this grid belongs to a shared size scope. - /// - /// True if the grid is registered in a shared size scope. - internal bool HasSharedSizeScope() - { - return _sharedSizeHost != null; - } - - /// - /// 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) - /// - /// - /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. - /// - internal void SharedScopeChanged() - { - _sharedSizeHost?.UnegisterGrid(this); - - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); - - if (scope != null) - { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); - } - - InvalidateMeasure(); - } - - /// - /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid - /// in it. - /// - /// The source of the event. - /// The event arguments. - private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - var scope = - new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); - - if (_sharedSizeHost != null) - throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); - - if (scope != null) - { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); - } - } - - /// - /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. - /// - /// The source of the event. - /// The event arguments. - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } - - - /// - /// Get the safe column/columnspan and safe row/rowspan. - /// This method ensures that none of the children has a column/row outside the bounds of the definitions. - /// - [Pure] - private (Dictionary safeColumns, - Dictionary safeRows) GetSafeColumnRows() - { - var columnCount = ColumnDefinitions.Count; - var rowCount = RowDefinitions.Count; - columnCount = columnCount == 0 ? 1 : columnCount; - rowCount = rowCount == 0 ? 1 : rowCount; - var safeColumns = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); - var safeRows = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); - return (safeColumns, safeRows); - } - - /// - /// Gets the safe row/column and rowspan/columnspan for a specified range. - /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. - /// - /// The row or column count. - /// The row or column that the user assigned. - /// The rowspan or columnspan that the user assigned. - /// The safe row/column and rowspan/columnspan. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) - { - var index = userIndex; - var span = userSpan; - - if (index < 0) - { - span = index + span; - index = 0; - } - - if (span <= 0) - { - span = 1; - } - - if (userIndex >= length) - { - index = length - 1; - span = 1; - } - else if (userIndex + userSpan > length) - { - span = length - userIndex; - } - - return (index, span); - } - - private static int ValidateColumn(AvaloniaObject o, int value) - { - if (value < 0) - { - throw new ArgumentException("Invalid Grid.Column value."); - } - - return value; - } - - private static int ValidateRow(AvaloniaObject o, int value) - { - if (value < 0) - { - throw new ArgumentException("Invalid Grid.Row value."); - } - - return value; - } - - /// - /// Called when the value of changes for a control. - /// - /// The control that triggered the change. - /// Change arguments. - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) - { - var shouldDispose = (arg2.OldValue is bool d) && d; - if (shouldDispose) - { - 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 shouldAssign = (arg2.NewValue is bool a) && a; - if (shouldAssign) - { - 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()); - } - - // if the scope has changed, notify the descendant grids that they need to update. - if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) - { - var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); - - foreach (var grid in participatingGrids) - grid.SharedScopeChanged(); - - } - } - } -} +// rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); +// columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); +// } + +// // Calculate for arrange result. +// var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); +// var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); +// // Arrange the children. +// foreach (var child in Children.OfType()) +// { +// 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)); +// } + +// // Assign the actual width. +// for (var i = 0; i < ColumnDefinitions.Count; i++) +// { +// ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; +// } + +// // Assign the actual height. +// for (var i = 0; i < RowDefinitions.Count; i++) +// { +// RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; +// } + +// // Return the render size. +// return finalSize; +// } + +// /// +// /// Tests whether this grid belongs to a shared size scope. +// /// +// /// True if the grid is registered in a shared size scope. +// internal bool HasSharedSizeScope() +// { +// return _sharedSizeHost != null; +// } + +// /// +// /// 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) +// /// +// /// +// /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. +// /// +// internal void SharedScopeChanged() +// { +// _sharedSizeHost?.UnegisterGrid(this); + +// _sharedSizeHost = null; +// var scope = this.GetVisualAncestors().OfType() +// .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + +// if (scope != null) +// { +// _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); +// _sharedSizeHost.RegisterGrid(this); +// } + +// InvalidateMeasure(); +// } + +// /// +// /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid +// /// in it. +// /// +// /// The source of the event. +// /// The event arguments. +// private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) +// { +// var scope = +// new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) +// .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + +// if (_sharedSizeHost != null) +// throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + +// if (scope != null) +// { +// _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); +// _sharedSizeHost.RegisterGrid(this); +// } +// } + +// /// +// /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. +// /// +// /// The source of the event. +// /// The event arguments. +// private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) +// { +// _sharedSizeHost?.UnegisterGrid(this); +// _sharedSizeHost = null; +// } + + +// /// +// /// Get the safe column/columnspan and safe row/rowspan. +// /// This method ensures that none of the children has a column/row outside the bounds of the definitions. +// /// +// [Pure] +// private (Dictionary safeColumns, +// Dictionary safeRows) GetSafeColumnRows() +// { +// var columnCount = ColumnDefinitions.Count; +// var rowCount = RowDefinitions.Count; +// columnCount = columnCount == 0 ? 1 : columnCount; +// rowCount = rowCount == 0 ? 1 : rowCount; +// var safeColumns = Children.OfType().ToDictionary(child => child, +// child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); +// var safeRows = Children.OfType().ToDictionary(child => child, +// child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); +// return (safeColumns, safeRows); +// } + +// /// +// /// Gets the safe row/column and rowspan/columnspan for a specified range. +// /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. +// /// +// /// The row or column count. +// /// The row or column that the user assigned. +// /// The rowspan or columnspan that the user assigned. +// /// The safe row/column and rowspan/columnspan. +// [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] +// private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) +// { +// var index = userIndex; +// var span = userSpan; + +// if (index < 0) +// { +// span = index + span; +// index = 0; +// } + +// if (span <= 0) +// { +// span = 1; +// } + +// if (userIndex >= length) +// { +// index = length - 1; +// span = 1; +// } +// else if (userIndex + userSpan > length) +// { +// span = length - userIndex; +// } + +// return (index, span); +// } + +// private static int ValidateColumn(AvaloniaObject o, int value) +// { +// if (value < 0) +// { +// throw new ArgumentException("Invalid Grid.Column value."); +// } + +// return value; +// } + +// private static int ValidateRow(AvaloniaObject o, int value) +// { +// if (value < 0) +// { +// throw new ArgumentException("Invalid Grid.Row value."); +// } + +// return value; +// } + +// /// +// /// Called when the value of changes for a control. +// /// +// /// The control that triggered the change. +// /// Change arguments. +// private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) +// { +// var shouldDispose = (arg2.OldValue is bool d) && d; +// if (shouldDispose) +// { +// 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 shouldAssign = (arg2.NewValue is bool a) && a; +// if (shouldAssign) +// { +// 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()); +// } + +// // if the scope has changed, notify the descendant grids that they need to update. +// if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) +// { +// var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); + +// foreach (var grid in participatingGrids) +// grid.SharedScopeChanged(); + +// } +// } +// } +// } diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index ff006a5c1b..fc498608e3 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -17,7 +17,7 @@ using Avalonia.Media; using Avalonia; using System.Collections; -namespace System.Windows.Controls +namespace Avalonia.Controls { /// /// Grid @@ -25,284 +25,108 @@ namespace System.Windows.Controls public class Grid : Panel { /// - /// + /// Defines the Column attached property. /// - 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)); - } - } + public static readonly AttachedProperty ColumnProperty = + AvaloniaProperty.RegisterAttached( + "Column", + validate: ValidateColumn); /// - /// Helper for setting Column property on a Control. + /// Defines the ColumnSpan attached property. /// - /// Control to set Column property on. - /// Column property value. - public static void SetColumn(Control element, int value) - { - if (element == null) - { - throw new ArgumentNullException("element"); - } - - element.SetValue(ColumnProperty, value); - } + public static readonly AttachedProperty ColumnSpanProperty = + AvaloniaProperty.RegisterAttached("ColumnSpan", 1); /// - /// Helper for reading Column property from a Control. + /// Defines the Row attached property. /// - /// Control to read Column property from. - /// Column property value. - public static int GetColumn(Control element) - { - if (element == null) - { - throw new ArgumentNullException("element"); - } - - return ((int)element.GetValue(ColumnProperty)); - } - - /// - /// 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) - { - if (element == null) - { - throw new ArgumentNullException("element"); - } - - element.SetValue(RowProperty, value); - } + public static readonly AttachedProperty RowProperty = + AvaloniaProperty.RegisterAttached( + "Row", + validate: ValidateRow); /// - /// Helper for reading Row property from a Control. + /// Defines the RowSpan attached property. /// - /// Control to read Row property from. - /// Row property value. - [AttachedPropertyBrowsableForChildren()] - public static int GetRow(Control element) - { - if (element == null) - { - throw new ArgumentNullException("element"); - } - - return ((int)element.GetValue(RowProperty)); - } - - /// - /// 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) - { - if (element == null) - { - throw new ArgumentNullException("element"); - } + public static readonly AttachedProperty RowSpanProperty = + AvaloniaProperty.RegisterAttached("RowSpan", 1); - element.SetValue(ColumnSpanProperty, value); - } + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); /// - /// Helper for reading ColumnSpan property from a Control. + /// ShowGridLines property. /// - /// Control to read ColumnSpan property from. - /// ColumnSpan property value. - [AttachedPropertyBrowsableForChildren()] - public static int GetColumnSpan(Control element) + public bool ShowGridLines { - if (element == null) - { - throw new ArgumentNullException("element"); - } - - return ((int)element.GetValue(ColumnSpanProperty)); + get { return (CheckFlagsAnd(Flags.ShowGridLinesPropertyValue)); } + set { SetValue(ShowGridLinesProperty, value); } } + private ColumnDefinitions _columnDefinitions; + private RowDefinitions _rowDefinitions; /// - /// Helper for setting RowSpan property on a Control. + /// Gets or sets the columns definitions for the grid. /// - /// Control to set RowSpan property on. - /// RowSpan property value. - public static void SetRowSpan(Control element, int value) + public ColumnDefinitions ColumnDefinitions { - if (element == null) + get { - throw new ArgumentNullException("element"); - } + if (_data == null) { _data = new ExtendedData(); } - element.SetValue(RowSpanProperty, value); - } + if (_columnDefinitions == null) + { + ColumnDefinitions = new ColumnDefinitions(); + } - /// - /// Helper for reading RowSpan property from a Control. - /// - /// Control to read RowSpan property from. - /// RowSpan property value. - public static int GetRowSpan(Control element) - { - if (element == null) - { - throw new ArgumentNullException("element"); + return _columnDefinitions; } - return ((int)element.GetValue(RowSpanProperty)); - } - - /// - /// 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) - { - if (element == null) + set { - throw new ArgumentNullException("element"); - } - element.SetValue(IsSharedSizeScopeProperty, value); - } + if (_columnDefinitions != null) + { + throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); + } - /// - /// Helper for reading IsSharedSizeScope property from a Control. - /// - /// Control to read IsSharedSizeScope property from. - /// IsSharedSizeScope property value. - public static bool GetIsSharedSizeScope(Control element) - { - if (element == null) - { - throw new ArgumentNullException("element"); + _columnDefinitions = value; + _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); } - - return ((bool)element.GetValue(IsSharedSizeScopeProperty)); - } - - - //------------------------------------------------------ - // - // Public Properties - // - //------------------------------------------------------ - - #region Public Properties - - /// - /// ShowGridLines property. - /// - public bool ShowGridLines - { - get { return (CheckFlagsAnd(Flags.ShowGridLinesPropertyValue)); } - set { SetValue(ShowGridLinesProperty, value); } } /// - /// Returns a ColumnDefinitionCollection of column definitions. + /// Gets or sets the row definitions for the grid. /// - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - public ColumnDefinitionCollection ColumnDefinitions + public RowDefinitions RowDefinitions { get { if (_data == null) { _data = new ExtendedData(); } - if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitionCollection(this); } - return (_data.ColumnDefinitions); - } - } - - /// - /// Returns a RowDefinitionCollection of row definitions. - /// - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - public RowDefinitionCollection RowDefinitions - { - get - { - if (_data == null) { _data = new ExtendedData(); } - if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitionCollection(this); } + if (_rowDefinitions == null) + { + RowDefinitions = new RowDefinitions(); + } - return (_data.RowDefinitions); + return _rowDefinitions; } - } - - #endregion Public Properties - - //------------------------------------------------------ - // - // Protected Methods - // - //------------------------------------------------------ - - #region Protected Methods - /// - /// Derived class must implement to support Visual Children. The method must return - /// the child at the specified index. Index must be between 0 and GetVisualChildrenCount-1. - /// - /// By default a Visual does not have any Children. - /// - /// Remark: - /// During this virtual call it is not valid to modify the Visual tree. - /// - protected override Visual GetVisualChild(int index) - { - // because "base.Count + 1" for GridLinesRenderer - // argument checking done at the base class - if (index == base.VisualChildrenCount) + set { - if (_gridLinesRenderer == null) + if (_rowDefinitions != null) { - throw new ArgumentOutOfRangeException("index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)); + throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); } - 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); } + _rowDefinitions = value; + _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + } } - /// /// Content measurement. /// @@ -315,8 +139,6 @@ namespace System.Windows.Controls try { - EnterCounterScope(Counters.MeasureOverride); - ListenToNotifications = true; MeasureOverrideInProgress = true; @@ -330,8 +152,9 @@ namespace System.Windows.Controls if (child != null) { child.Measure(constraint); - gridDesiredSize.Width = Math.Max(gridDesiredSize.Width, child.DesiredSize.Width); - gridDesiredSize.Height = Math.Max(gridDesiredSize.Height, child.DesiredSize.Height); + gridDesiredSize = new Size( + Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); } } } @@ -590,17 +413,14 @@ namespace System.Windows.Controls MeasureCellsGroup(extData.CellGroup4, constraint, false, false); - EnterCounter(Counters._CalculateDesiredSize); gridDesiredSize = new Size( CalculateDesiredSize(DefinitionsU), CalculateDesiredSize(DefinitionsV)); - ExitCounter(Counters._CalculateDesiredSize); } } finally { MeasureOverrideInProgress = false; - ExitCounterScope(Counters.MeasureOverride); } return (gridDesiredSize); @@ -614,17 +434,14 @@ namespace System.Windows.Controls { try { - EnterCounterScope(Counters.ArrangeOverride); ArrangeOverrideInProgress = true; if (_data == null) { - ControlCollection Children = InternalChildren; - for (int i = 0, count = Children.Count; i < count; ++i) { - Control child = Children[i]; + var child = Children[i]; if (child != null) { child.Arrange(new Rect(arrangeSize)); @@ -635,18 +452,12 @@ namespace System.Windows.Controls { Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); - EnterCounter(Counters._SetFinalSize); - SetFinalSize(DefinitionsU, arrangeSize.Width, true); SetFinalSize(DefinitionsV, arrangeSize.Height, false); - ExitCounter(Counters._SetFinalSize); - - ControlCollection Children = InternalChildren; - for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) { - Control cell = Children[currentCell]; + IControl cell = Children[currentCell]; if (cell == null) { continue; @@ -663,9 +474,7 @@ namespace System.Windows.Controls GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); - EnterCounter(Counters._ArrangeChildHelper2); cell.Arrange(cellRect); - ExitCounter(Counters._ArrangeChildHelper2); } // update render bound on grid lines renderer visual @@ -680,7 +489,6 @@ namespace System.Windows.Controls { SetValid(); ArrangeOverrideInProgress = false; - ExitCounterScope(Counters.ArrangeOverride); } return (arrangeSize); } @@ -697,7 +505,6 @@ namespace System.Windows.Controls base.OnVisualChildrenChanged(visualAdded, visualRemoved); } - #endregion Protected Methods //------------------------------------------------------ // @@ -821,15 +628,11 @@ namespace System.Windows.Controls /// private void ValidateCells() { - EnterCounter(Counters._ValidateCells); - if (CellsStructureDirty) { ValidateCellsCore(); CellsStructureDirty = false; } - - ExitCounter(Counters._ValidateCells); } /// @@ -837,7 +640,6 @@ namespace System.Windows.Controls /// private void ValidateCellsCore() { - ControlCollection Children = InternalChildren; ExtendedData extData = ExtData; extData.CellCachesCollection = new CellCache[Children.Count]; @@ -852,7 +654,7 @@ namespace System.Windows.Controls for (int i = PrivateCells.Length - 1; i >= 0; --i) { - Control child = Children[i]; + var child = Children[i]; if (child == null) { continue; @@ -1312,7 +1114,7 @@ namespace System.Windows.Controls } EnterCounter(Counters.__MeasureChild); - Control child = InternalChildren[cell]; + IControl child = InternalChildren[cell]; if (child != null) { Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); @@ -2071,7 +1873,7 @@ namespace System.Windows.Controls if (useLayoutRounding) { roundingErrors[i] = definitions[i].SizeCache; - definitions[i].SizeCache = Control.RoundLayoutValue(definitions[i].SizeCache, dpi); + definitions[i].SizeCache = IControl.RoundLayoutValue(definitions[i].SizeCache, dpi); } } definitionIndices[starDefinitionsCount++] = i; @@ -2110,7 +1912,7 @@ namespace System.Windows.Controls if (useLayoutRounding) { roundingErrors[i] = definitions[i].SizeCache; - definitions[i].SizeCache = Control.RoundLayoutValue(definitions[i].SizeCache, dpi); + definitions[i].SizeCache = IControl.RoundLayoutValue(definitions[i].SizeCache, dpi); } allPreferredArrangeSize += definitions[i].SizeCache; @@ -2161,7 +1963,7 @@ namespace System.Windows.Controls if (useLayoutRounding) { roundingErrors[definitionIndices[i]] = definitions[definitionIndices[i]].SizeCache; - definitions[definitionIndices[i]].SizeCache = Control.RoundLayoutValue(definitions[definitionIndices[i]].SizeCache, dpi); + definitions[definitionIndices[i]].SizeCache = IControl.RoundLayoutValue(definitions[definitionIndices[i]].SizeCache, dpi); } allPreferredArrangeSize += definitions[definitionIndices[i]].SizeCache; @@ -2186,7 +1988,7 @@ namespace System.Windows.Controls if (useLayoutRounding) { roundingErrors[definitionIndex] = final; - final = Control.RoundLayoutValue(finalOld, dpi); + final = IControl.RoundLayoutValue(finalOld, dpi); final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); final = Math.Min(final, definitions[definitionIndex].SizeCache); } @@ -2213,7 +2015,7 @@ namespace System.Windows.Controls RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); double adjustedSize = allPreferredArrangeSize; - double dpiIncrement = Control.RoundLayoutValue(1.0, dpi); + double dpiIncrement = IControl.RoundLayoutValue(1.0, dpi); if (allPreferredArrangeSize > finalSize) { @@ -2612,7 +2414,7 @@ namespace System.Windows.Controls for (int i = 0; i < definitions.Length; ++i) { DefinitionBase def = definitions[i]; - double roundedSize = Control.RoundLayoutValue(def.SizeCache, dpi); + double roundedSize = IControl.RoundLayoutValue(def.SizeCache, dpi); roundingErrors[i] = (roundedSize - def.SizeCache); def.SizeCache = roundedSize; roundedTakenSize += roundedSize; @@ -2826,29 +2628,6 @@ namespace System.Windows.Controls } } - /// - /// Returns true if ColumnDefinitions collection is not empty - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public bool ShouldSerializeColumnDefinitions() - { - ExtendedData extData = ExtData; - return (extData != null - && extData.ColumnDefinitions != null - && extData.ColumnDefinitions.Count > 0); - } - - /// - /// Returns true if RowDefinitions collection is not empty - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public bool ShouldSerializeRowDefinitions() - { - ExtendedData extData = ExtData; - return (extData != null - && extData.RowDefinitions != null - && extData.RowDefinitions.Count > 0); - } /// /// Synchronized ShowGridLines property with the state of the grid's visual collection @@ -2991,16 +2770,6 @@ namespace System.Windows.Controls return (result != 2); } - #endregion Private Methods - - //------------------------------------------------------ - // - // Private Properties - // - //------------------------------------------------------ - - #region Private Properties - /// /// Private version returning array of column definitions. /// @@ -4036,27 +3805,6 @@ namespace System.Windows.Controls return (false); } - public Object Current - { - get - { - if (_currentEnumerator == -1) - { -#pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception - throw new InvalidOperationException(SR.Get(SRID.EnumeratorNotStarted)); - } - if (_currentEnumerator >= 3) - { -#pragma warning suppress 6503 // IEnumerator.Current is documented to throw this exception - throw new InvalidOperationException(SR.Get(SRID.EnumeratorReachedEnd)); - } - - // assert below is not true anymore since ControlCollection allowes for null Children - //Debug.Assert(_currentChild != null); - return (_currentChild); - } - } - public void Reset() { _currentEnumerator = -1; @@ -4070,7 +3818,7 @@ namespace System.Windows.Controls private Object _currentChild; private ColumnDefinitionCollection.Enumerator _enumerator0; private RowDefinitionCollection.Enumerator _enumerator1; - private ControlCollection _enumerator2Collection; + private Controls _enumerator2Collection; private int _enumerator2Index; private int _enumerator2Count; } @@ -4157,7 +3905,5 @@ namespace System.Windows.Controls private static readonly Pen s_evenDashPen; // second pen to draw dash private static readonly Point c_zeroPoint = new Point(0, 0); } - - #endregion Private Structures Classes } } \ No newline at end of file From f9ebfc232d685a9fe8e06c305c1cd52a0058c287 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 23 May 2019 14:00:08 +0800 Subject: [PATCH 029/179] Uncomment Avalonia's Grid and exclude it from build temporarily. --- .../Avalonia.Controls.csproj | 1 + src/Avalonia.Controls/Grid.cs | 1199 ++++++++--------- 2 files changed, 600 insertions(+), 600 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 32331d29ab..eabe136791 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -14,4 +14,5 @@ + diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 98cd5aad02..ad5e96376d 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1,601 +1,600 @@ -// // 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.Generic; -// using System.Diagnostics; -// using System.Linq; -// using System.Reactive.Linq; -// using System.Runtime.CompilerServices; -// using Avalonia.Collections; -// using Avalonia.Controls.Utils; -// using Avalonia.VisualTree; -// using JetBrains.Annotations; - -// namespace Avalonia.Controls -// { -// /// -// /// Lays out child controls according to a grid. -// /// -// public class Grid : Panel -// { -// /// -// /// Defines the Column attached property. -// /// -// public static readonly AttachedProperty ColumnProperty = -// AvaloniaProperty.RegisterAttached( -// "Column", -// validate: ValidateColumn); - -// /// -// /// Defines the ColumnSpan attached property. -// /// -// public static readonly AttachedProperty ColumnSpanProperty = -// AvaloniaProperty.RegisterAttached("ColumnSpan", 1); - -// /// -// /// Defines the Row attached property. -// /// -// public static readonly AttachedProperty RowProperty = -// AvaloniaProperty.RegisterAttached( -// "Row", -// validate: ValidateRow); - -// /// -// /// Defines the RowSpan attached property. -// /// -// public static readonly AttachedProperty RowSpanProperty = -// AvaloniaProperty.RegisterAttached("RowSpan", 1); - -// public static readonly AttachedProperty IsSharedSizeScopeProperty = -// AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - -// protected override void OnMeasureInvalidated() -// { -// base.OnMeasureInvalidated(); -// _sharedSizeHost?.InvalidateMeasure(this); -// } - -// private SharedSizeScopeHost _sharedSizeHost; - -// /// -// /// Defines the SharedSizeScopeHost private property. -// /// The ampersands are used to make accessing the property via xaml inconvenient. -// /// -// internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = -// AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); - -// private ColumnDefinitions _columnDefinitions; - -// private RowDefinitions _rowDefinitions; - -// static Grid() -// { -// AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); -// IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); -// } - -// public Grid() -// { -// this.AttachedToVisualTree += Grid_AttachedToVisualTree; -// this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; -// } - -// /// -// /// Gets or sets the columns definitions for the grid. -// /// -// public ColumnDefinitions ColumnDefinitions -// { -// get -// { -// if (_columnDefinitions == null) -// { -// ColumnDefinitions = new ColumnDefinitions(); -// } - -// return _columnDefinitions; -// } - -// set -// { -// if (_columnDefinitions != null) -// { -// throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); -// } - -// _columnDefinitions = value; -// _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); -// _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); -// } -// } - -// /// -// /// Gets or sets the row definitions for the grid. -// /// -// public RowDefinitions RowDefinitions -// { -// get -// { -// if (_rowDefinitions == null) -// { -// RowDefinitions = new RowDefinitions(); -// } - -// return _rowDefinitions; -// } - -// set -// { -// if (_rowDefinitions != null) -// { -// throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); -// } - -// _rowDefinitions = value; -// _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); -// _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); -// } -// } - -// /// -// /// Gets the value of the Column attached property for a control. -// /// -// /// The control. -// /// The control's column. -// public static int GetColumn(AvaloniaObject element) -// { -// return element.GetValue(ColumnProperty); -// } - -// /// -// /// Gets the value of the ColumnSpan attached property for a control. -// /// -// /// The control. -// /// The control's column span. -// public static int GetColumnSpan(AvaloniaObject element) -// { -// return element.GetValue(ColumnSpanProperty); -// } - -// /// -// /// Gets the value of the Row attached property for a control. -// /// -// /// The control. -// /// The control's row. -// public static int GetRow(AvaloniaObject element) -// { -// return element.GetValue(RowProperty); -// } - -// /// -// /// Gets the value of the RowSpan attached property for a control. -// /// -// /// The control. -// /// The control's row span. -// public static int GetRowSpan(AvaloniaObject element) -// { -// return element.GetValue(RowSpanProperty); -// } - - -// /// -// /// Gets the value of the IsSharedSizeScope attached property for a control. -// /// -// /// The control. -// /// The control's IsSharedSizeScope value. -// public static bool GetIsSharedSizeScope(AvaloniaObject element) -// { -// return element.GetValue(IsSharedSizeScopeProperty); -// } - -// /// -// /// Sets the value of the Column attached property for a control. -// /// -// /// The control. -// /// The column value. -// public static void SetColumn(AvaloniaObject element, int value) -// { -// element.SetValue(ColumnProperty, value); -// } - -// /// -// /// Sets the value of the ColumnSpan attached property for a control. -// /// -// /// The control. -// /// The column span value. -// public static void SetColumnSpan(AvaloniaObject element, int value) -// { -// element.SetValue(ColumnSpanProperty, value); -// } - -// /// -// /// Sets the value of the Row attached property for a control. -// /// -// /// The control. -// /// The row value. -// public static void SetRow(AvaloniaObject element, int value) -// { -// element.SetValue(RowProperty, value); -// } - -// /// -// /// Sets the value of the RowSpan attached property for a control. -// /// -// /// The control. -// /// The row span value. -// public static void SetRowSpan(AvaloniaObject element, int value) -// { -// element.SetValue(RowSpanProperty, value); -// } - -// /// -// /// Sets the value of IsSharedSizeScope property for a control. -// /// -// /// The control. -// /// The IsSharedSizeScope value. -// public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) -// { -// element.SetValue(IsSharedSizeScopeProperty, value); -// } - -// /// -// /// Gets the result of the last column measurement. -// /// Use this result to reduce the arrange calculation. -// /// -// private GridLayout.MeasureResult _columnMeasureCache; - -// /// -// /// Gets the result of the last row measurement. -// /// Use this result to reduce the arrange calculation. -// /// -// private GridLayout.MeasureResult _rowMeasureCache; - -// /// -// /// Gets the row layout as of the last measure. -// /// -// private GridLayout _rowLayoutCache; - -// /// -// /// Gets the column layout as of the last measure. -// /// -// private GridLayout _columnLayoutCache; - -// /// -// /// Measures the grid. -// /// -// /// 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. - -// 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); -// } - -// maxWidth = Math.Min(maxWidth, constraint.Width); -// maxHeight = Math.Min(maxHeight, constraint.Height); -// return new Size(maxWidth, maxHeight); -// } - -// // Situation 2/2: -// // If the grid defines some columns or rows. -// // Debug Tip: -// // - GridLayout doesn't hold any state, so you can drag the debugger execution -// // arrow back to any statements and re-run them without any side-effect. - -// var measureCache = new Dictionary(); -// var (safeColumns, safeRows) = GetSafeColumnRows(); -// var columnLayout = new GridLayout(ColumnDefinitions); -// var rowLayout = new GridLayout(RowDefinitions); -// // Note: If a child stays in a * or Auto column/row, use constraint to measure it. -// columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); -// rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); - -// // Calculate measurement. -// var columnResult = columnLayout.Measure(constraint.Width); -// var rowResult = rowLayout.Measure(constraint.Height); - -// // Use the results of the measurement to measure the rest of the children. -// foreach (var child in Children.OfType()) -// { -// var (column, columnSpan) = safeColumns[child]; -// var (row, rowSpan) = safeRows[child]; -// var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); -// var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); - -// MeasureOnce(child, new Size(width, height)); -// } - -// // Cache the measure result and return the desired size. -// _columnMeasureCache = columnResult; -// _rowMeasureCache = rowResult; -// _rowLayoutCache = rowLayout; -// _columnLayoutCache = columnLayout; - -// if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) -// { -// _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); -// } - -// return new Size(columnResult.DesiredLength, rowResult.DesiredLength); - -// // Measure each child only once. -// // If a child has been measured, it will just return the desired size. -// Size MeasureOnce(Control child, Size size) -// { -// if (measureCache.TryGetValue(child, out var desiredSize)) -// { -// return desiredSize; -// } - -// child.Measure(size); -// desiredSize = child.DesiredSize; -// measureCache[child] = desiredSize; -// return desiredSize; -// } -// } - -// /// -// /// Arranges the grid's children. -// /// -// /// The size allocated to the control. -// /// The space taken. -// protected override Size ArrangeOverride(Size finalSize) -// { -// // 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) -// { -// foreach (var child in Children.OfType()) -// { -// child.Arrange(new Rect(finalSize)); -// } - -// return finalSize; -// } - -// // Situation 2/2: -// // If the grid defines some columns or rows. -// // Debug Tip: -// // - GridLayout doesn't hold any state, so you can drag the debugger execution -// // arrow back to any statements and re-run them without any side-effect. - -// var (safeColumns, safeRows) = GetSafeColumnRows(); -// var columnLayout = _columnLayoutCache; -// var rowLayout = _rowLayoutCache; - -// var rowCache = _rowMeasureCache; -// var columnCache = _columnMeasureCache; - -// if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) -// { -// (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); +// 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.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Linq; +using System.Runtime.CompilerServices; +using Avalonia.Collections; +using Avalonia.Controls.Utils; +using Avalonia.VisualTree; +using JetBrains.Annotations; + +namespace Avalonia.Controls +{ + /// + /// Lays out child controls according to a grid. + /// + public class Grid : Panel + { + /// + /// Defines the Column attached property. + /// + public static readonly AttachedProperty ColumnProperty = + AvaloniaProperty.RegisterAttached( + "Column", + validate: ValidateColumn); + + /// + /// Defines the ColumnSpan attached property. + /// + public static readonly AttachedProperty ColumnSpanProperty = + AvaloniaProperty.RegisterAttached("ColumnSpan", 1); + + /// + /// Defines the Row attached property. + /// + public static readonly AttachedProperty RowProperty = + AvaloniaProperty.RegisterAttached( + "Row", + validate: ValidateRow); + + /// + /// Defines the RowSpan attached property. + /// + public static readonly AttachedProperty RowSpanProperty = + AvaloniaProperty.RegisterAttached("RowSpan", 1); + + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + + protected override void OnMeasureInvalidated() + { + base.OnMeasureInvalidated(); + _sharedSizeHost?.InvalidateMeasure(this); + } + + private SharedSizeScopeHost _sharedSizeHost; + + /// + /// Defines the SharedSizeScopeHost private property. + /// The ampersands are used to make accessing the property via xaml inconvenient. + /// + internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = + AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); + + private ColumnDefinitions _columnDefinitions; + + private RowDefinitions _rowDefinitions; + + static Grid() + { + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); + } + + public Grid() + { + this.AttachedToVisualTree += Grid_AttachedToVisualTree; + this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; + } + + /// + /// Gets or sets the columns definitions for the grid. + /// + public ColumnDefinitions ColumnDefinitions + { + get + { + if (_columnDefinitions == null) + { + ColumnDefinitions = new ColumnDefinitions(); + } + + return _columnDefinitions; + } + + set + { + if (_columnDefinitions != null) + { + throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); + } + + _columnDefinitions = value; + _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + } + } + + /// + /// Gets or sets the row definitions for the grid. + /// + public RowDefinitions RowDefinitions + { + get + { + if (_rowDefinitions == null) + { + RowDefinitions = new RowDefinitions(); + } + + return _rowDefinitions; + } + + set + { + if (_rowDefinitions != null) + { + throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); + } + + _rowDefinitions = value; + _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + } + } + + /// + /// Gets the value of the Column attached property for a control. + /// + /// The control. + /// The control's column. + public static int GetColumn(AvaloniaObject element) + { + return element.GetValue(ColumnProperty); + } + + /// + /// Gets the value of the ColumnSpan attached property for a control. + /// + /// The control. + /// The control's column span. + public static int GetColumnSpan(AvaloniaObject element) + { + return element.GetValue(ColumnSpanProperty); + } + + /// + /// Gets the value of the Row attached property for a control. + /// + /// The control. + /// The control's row. + public static int GetRow(AvaloniaObject element) + { + return element.GetValue(RowProperty); + } + + /// + /// Gets the value of the RowSpan attached property for a control. + /// + /// The control. + /// The control's row span. + public static int GetRowSpan(AvaloniaObject element) + { + return element.GetValue(RowSpanProperty); + } + + + /// + /// Gets the value of the IsSharedSizeScope attached property for a control. + /// + /// The control. + /// The control's IsSharedSizeScope value. + public static bool GetIsSharedSizeScope(AvaloniaObject element) + { + return element.GetValue(IsSharedSizeScopeProperty); + } + + /// + /// Sets the value of the Column attached property for a control. + /// + /// The control. + /// The column value. + public static void SetColumn(AvaloniaObject element, int value) + { + element.SetValue(ColumnProperty, value); + } + + /// + /// Sets the value of the ColumnSpan attached property for a control. + /// + /// The control. + /// The column span value. + public static void SetColumnSpan(AvaloniaObject element, int value) + { + element.SetValue(ColumnSpanProperty, value); + } + + /// + /// Sets the value of the Row attached property for a control. + /// + /// The control. + /// The row value. + public static void SetRow(AvaloniaObject element, int value) + { + element.SetValue(RowProperty, value); + } + + /// + /// Sets the value of the RowSpan attached property for a control. + /// + /// The control. + /// The row span value. + public static void SetRowSpan(AvaloniaObject element, int value) + { + element.SetValue(RowSpanProperty, value); + } + + /// + /// Sets the value of IsSharedSizeScope property for a control. + /// + /// The control. + /// The IsSharedSizeScope value. + public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) + { + element.SetValue(IsSharedSizeScopeProperty, value); + } + + /// + /// Gets the result of the last column measurement. + /// Use this result to reduce the arrange calculation. + /// + private GridLayout.MeasureResult _columnMeasureCache; + + /// + /// Gets the result of the last row measurement. + /// Use this result to reduce the arrange calculation. + /// + private GridLayout.MeasureResult _rowMeasureCache; + + /// + /// Gets the row layout as of the last measure. + /// + private GridLayout _rowLayoutCache; + + /// + /// Gets the column layout as of the last measure. + /// + private GridLayout _columnLayoutCache; + + /// + /// Measures the grid. + /// + /// 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. + + 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); + } + + maxWidth = Math.Min(maxWidth, constraint.Width); + maxHeight = Math.Min(maxHeight, constraint.Height); + return new Size(maxWidth, maxHeight); + } + + // Situation 2/2: + // If the grid defines some columns or rows. + // Debug Tip: + // - GridLayout doesn't hold any state, so you can drag the debugger execution + // arrow back to any statements and re-run them without any side-effect. + + var measureCache = new Dictionary(); + var (safeColumns, safeRows) = GetSafeColumnRows(); + var columnLayout = new GridLayout(ColumnDefinitions); + var rowLayout = new GridLayout(RowDefinitions); + // Note: If a child stays in a * or Auto column/row, use constraint to measure it. + columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); + rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); + + // Calculate measurement. + var columnResult = columnLayout.Measure(constraint.Width); + var rowResult = rowLayout.Measure(constraint.Height); + + // Use the results of the measurement to measure the rest of the children. + foreach (var child in Children.OfType()) + { + var (column, columnSpan) = safeColumns[child]; + var (row, rowSpan) = safeRows[child]; + var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); + var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); + + MeasureOnce(child, new Size(width, height)); + } + + // Cache the measure result and return the desired size. + _columnMeasureCache = columnResult; + _rowMeasureCache = rowResult; + _rowLayoutCache = rowLayout; + _columnLayoutCache = columnLayout; + + if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + { + _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + } + + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); + + // Measure each child only once. + // If a child has been measured, it will just return the desired size. + Size MeasureOnce(Control child, Size size) + { + if (measureCache.TryGetValue(child, out var desiredSize)) + { + return desiredSize; + } + + child.Measure(size); + desiredSize = child.DesiredSize; + measureCache[child] = desiredSize; + return desiredSize; + } + } + + /// + /// Arranges the grid's children. + /// + /// The size allocated to the control. + /// The space taken. + protected override Size ArrangeOverride(Size finalSize) + { + // 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) + { + foreach (var child in Children.OfType()) + { + child.Arrange(new Rect(finalSize)); + } + + return finalSize; + } + + // Situation 2/2: + // If the grid defines some columns or rows. + // Debug Tip: + // - GridLayout doesn't hold any state, so you can drag the debugger execution + // arrow back to any statements and re-run them without any side-effect. + + var (safeColumns, safeRows) = GetSafeColumnRows(); + var columnLayout = _columnLayoutCache; + var rowLayout = _rowLayoutCache; + + var rowCache = _rowMeasureCache; + var columnCache = _columnMeasureCache; + + if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + { + (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); -// rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); -// columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); -// } - -// // Calculate for arrange result. -// var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); -// var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); -// // Arrange the children. -// foreach (var child in Children.OfType()) -// { -// 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)); -// } - -// // Assign the actual width. -// for (var i = 0; i < ColumnDefinitions.Count; i++) -// { -// ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; -// } - -// // Assign the actual height. -// for (var i = 0; i < RowDefinitions.Count; i++) -// { -// RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; -// } - -// // Return the render size. -// return finalSize; -// } - -// /// -// /// Tests whether this grid belongs to a shared size scope. -// /// -// /// True if the grid is registered in a shared size scope. -// internal bool HasSharedSizeScope() -// { -// return _sharedSizeHost != null; -// } - -// /// -// /// 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) -// /// -// /// -// /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. -// /// -// internal void SharedScopeChanged() -// { -// _sharedSizeHost?.UnegisterGrid(this); - -// _sharedSizeHost = null; -// var scope = this.GetVisualAncestors().OfType() -// .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); - -// if (scope != null) -// { -// _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); -// _sharedSizeHost.RegisterGrid(this); -// } - -// InvalidateMeasure(); -// } - -// /// -// /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid -// /// in it. -// /// -// /// The source of the event. -// /// The event arguments. -// private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) -// { -// var scope = -// new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) -// .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); - -// if (_sharedSizeHost != null) -// throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); - -// if (scope != null) -// { -// _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); -// _sharedSizeHost.RegisterGrid(this); -// } -// } - -// /// -// /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. -// /// -// /// The source of the event. -// /// The event arguments. -// private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) -// { -// _sharedSizeHost?.UnegisterGrid(this); -// _sharedSizeHost = null; -// } - - -// /// -// /// Get the safe column/columnspan and safe row/rowspan. -// /// This method ensures that none of the children has a column/row outside the bounds of the definitions. -// /// -// [Pure] -// private (Dictionary safeColumns, -// Dictionary safeRows) GetSafeColumnRows() -// { -// var columnCount = ColumnDefinitions.Count; -// var rowCount = RowDefinitions.Count; -// columnCount = columnCount == 0 ? 1 : columnCount; -// rowCount = rowCount == 0 ? 1 : rowCount; -// var safeColumns = Children.OfType().ToDictionary(child => child, -// child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); -// var safeRows = Children.OfType().ToDictionary(child => child, -// child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); -// return (safeColumns, safeRows); -// } - -// /// -// /// Gets the safe row/column and rowspan/columnspan for a specified range. -// /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. -// /// -// /// The row or column count. -// /// The row or column that the user assigned. -// /// The rowspan or columnspan that the user assigned. -// /// The safe row/column and rowspan/columnspan. -// [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] -// private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) -// { -// var index = userIndex; -// var span = userSpan; - -// if (index < 0) -// { -// span = index + span; -// index = 0; -// } - -// if (span <= 0) -// { -// span = 1; -// } - -// if (userIndex >= length) -// { -// index = length - 1; -// span = 1; -// } -// else if (userIndex + userSpan > length) -// { -// span = length - userIndex; -// } - -// return (index, span); -// } - -// private static int ValidateColumn(AvaloniaObject o, int value) -// { -// if (value < 0) -// { -// throw new ArgumentException("Invalid Grid.Column value."); -// } - -// return value; -// } - -// private static int ValidateRow(AvaloniaObject o, int value) -// { -// if (value < 0) -// { -// throw new ArgumentException("Invalid Grid.Row value."); -// } - -// return value; -// } - -// /// -// /// Called when the value of changes for a control. -// /// -// /// The control that triggered the change. -// /// Change arguments. -// private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) -// { -// var shouldDispose = (arg2.OldValue is bool d) && d; -// if (shouldDispose) -// { -// 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 shouldAssign = (arg2.NewValue is bool a) && a; -// if (shouldAssign) -// { -// 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()); -// } - -// // if the scope has changed, notify the descendant grids that they need to update. -// if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) -// { -// var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); - -// foreach (var grid in participatingGrids) -// grid.SharedScopeChanged(); - -// } -// } -// } -// } + rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); + columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); + } + + // Calculate for arrange result. + var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); + var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); + // Arrange the children. + foreach (var child in Children.OfType()) + { + 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)); + } + + // Assign the actual width. + for (var i = 0; i < ColumnDefinitions.Count; i++) + { + ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; + } + + // Assign the actual height. + for (var i = 0; i < RowDefinitions.Count; i++) + { + RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; + } + + // Return the render size. + return finalSize; + } + + /// + /// Tests whether this grid belongs to a shared size scope. + /// + /// True if the grid is registered in a shared size scope. + internal bool HasSharedSizeScope() + { + return _sharedSizeHost != null; + } + + /// + /// 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) + /// + /// + /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. + /// + internal void SharedScopeChanged() + { + _sharedSizeHost?.UnegisterGrid(this); + + _sharedSizeHost = null; + var scope = this.GetVisualAncestors().OfType() + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + + InvalidateMeasure(); + } + + /// + /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid + /// in it. + /// + /// The source of the event. + /// The event arguments. + private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + var scope = + new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (_sharedSizeHost != null) + throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + } + + /// + /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. + /// + /// The source of the event. + /// The event arguments. + private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + { + _sharedSizeHost?.UnegisterGrid(this); + _sharedSizeHost = null; + } + + + /// + /// Get the safe column/columnspan and safe row/rowspan. + /// This method ensures that none of the children has a column/row outside the bounds of the definitions. + /// + [Pure] + private (Dictionary safeColumns, + Dictionary safeRows) GetSafeColumnRows() + { + var columnCount = ColumnDefinitions.Count; + var rowCount = RowDefinitions.Count; + columnCount = columnCount == 0 ? 1 : columnCount; + rowCount = rowCount == 0 ? 1 : rowCount; + var safeColumns = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); + var safeRows = Children.OfType().ToDictionary(child => child, + child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); + return (safeColumns, safeRows); + } + + /// + /// Gets the safe row/column and rowspan/columnspan for a specified range. + /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. + /// + /// The row or column count. + /// The row or column that the user assigned. + /// The rowspan or columnspan that the user assigned. + /// The safe row/column and rowspan/columnspan. + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) + { + var index = userIndex; + var span = userSpan; + + if (index < 0) + { + span = index + span; + index = 0; + } + + if (span <= 0) + { + span = 1; + } + + if (userIndex >= length) + { + index = length - 1; + span = 1; + } + else if (userIndex + userSpan > length) + { + span = length - userIndex; + } + + return (index, span); + } + + private static int ValidateColumn(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Column value."); + } + + return value; + } + + private static int ValidateRow(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Row value."); + } + + return value; + } + + /// + /// Called when the value of changes for a control. + /// + /// The control that triggered the change. + /// Change arguments. + private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + { + var shouldDispose = (arg2.OldValue is bool d) && d; + if (shouldDispose) + { + 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 shouldAssign = (arg2.NewValue is bool a) && a; + if (shouldAssign) + { + 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()); + } + + // if the scope has changed, notify the descendant grids that they need to update. + if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) + { + var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); + + foreach (var grid in participatingGrids) + grid.SharedScopeChanged(); + } + } + } +} From d39d132d103e0a2c6a1309418b979e51b077ab44 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 23 May 2019 14:09:12 +0800 Subject: [PATCH 030/179] Part 3 of n --- src/Avalonia.Controls/GridWPF.cs | 97 +++++++------------------------- 1 file changed, 21 insertions(+), 76 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index fc498608e3..0c8f571506 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -165,7 +165,7 @@ namespace Avalonia.Controls bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); // Clear index information and rounding errors - if (RowDefinitionCollectionDirty || ColumnDefinitionCollectionDirty) + if (RowDefinitionsDirty || ColumnDefinitionsDirty) { if (_definitionIndices != null) { @@ -493,27 +493,6 @@ namespace Avalonia.Controls return (arrangeSize); } - /// - /// - /// - protected internal override void OnVisualChildrenChanged( - DependencyObject visualAdded, - DependencyObject visualRemoved) - { - CellsStructureDirty = true; - - base.OnVisualChildrenChanged(visualAdded, visualRemoved); - } - - - //------------------------------------------------------ - // - // Internal Methods - // - //------------------------------------------------------ - - #region Internal Methods - /// /// Invalidates grid caches and makes the grid dirty for measure. /// @@ -533,10 +512,10 @@ namespace Avalonia.Controls { double value = 0.0; - Invariant.Assert(_data != null); + Contract.Requires(_data != null); // actual value calculations require structure to be up-to-date - if (!ColumnDefinitionCollectionDirty) + if (!ColumnDefinitionsDirty) { DefinitionBase[] definitions = DefinitionsU; value = definitions[(columnIndex + 1) % definitions.Length].FinalOffset; @@ -555,10 +534,10 @@ namespace Avalonia.Controls { double value = 0.0; - Invariant.Assert(_data != null); + Contract.Requires(_data != null); // actual value calculations require structure to be up-to-date - if (!RowDefinitionCollectionDirty) + if (!RowDefinitionsDirty) { DefinitionBase[] definitions = DefinitionsV; value = definitions[(rowIndex + 1) % definitions.Length].FinalOffset; @@ -567,16 +546,6 @@ namespace Avalonia.Controls return (value); } - #endregion Internal Methods - - //------------------------------------------------------ - // - // Internal Properties - // - //------------------------------------------------------ - - #region Internal Properties - /// /// Convenience accessor to MeasureOverrideInProgress bit flag. /// @@ -598,7 +567,7 @@ namespace Avalonia.Controls /// /// Convenience accessor to ValidDefinitionsUStructure bit flag. /// - internal bool ColumnDefinitionCollectionDirty + internal bool ColumnDefinitionsDirty { get { return (!CheckFlagsAnd(Flags.ValidDefinitionsUStructure)); } set { SetFlags(!value, Flags.ValidDefinitionsUStructure); } @@ -607,22 +576,12 @@ namespace Avalonia.Controls /// /// Convenience accessor to ValidDefinitionsVStructure bit flag. /// - internal bool RowDefinitionCollectionDirty + internal bool RowDefinitionsDirty { get { return (!CheckFlagsAnd(Flags.ValidDefinitionsVStructure)); } set { SetFlags(!value, Flags.ValidDefinitionsVStructure); } } - #endregion Internal Properties - - //------------------------------------------------------ - // - // Private Methods - // - //------------------------------------------------------ - - #region Private Methods - /// /// Lays out cells according to rows and columns, and creates lookup grids. /// @@ -665,11 +624,11 @@ namespace Avalonia.Controls // // read and cache child positioning properties // - + // read indices from the corresponding properties // clamp to value < number_of_columns // column >= 0 is guaranteed by property value validation callback - cell.ColumnIndex = Math.Min(GetColumn(child), DefinitionsU.Length - 1); + cell.ColumnIndex = Math.Min(Grid.ColumnProperty (child), DefinitionsU.Length - 1); // clamp to value < number_of_rows // row >= 0 is guaranteed by property value validation callback cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Length - 1); @@ -750,9 +709,7 @@ namespace Avalonia.Controls /// private void ValidateDefinitionsUStructure() { - EnterCounter(Counters._ValidateColsStructure); - - if (ColumnDefinitionCollectionDirty) + if (ColumnDefinitionsDirty) { ExtendedData extData = ExtData; @@ -779,12 +736,10 @@ namespace Avalonia.Controls } } - ColumnDefinitionCollectionDirty = false; + ColumnDefinitionsDirty = false; } Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Length > 0); - - ExitCounter(Counters._ValidateColsStructure); } /// @@ -797,9 +752,7 @@ namespace Avalonia.Controls /// private void ValidateDefinitionsVStructure() { - EnterCounter(Counters._ValidateRowsStructure); - - if (RowDefinitionCollectionDirty) + if (RowDefinitionsDirty) { ExtendedData extData = ExtData; @@ -826,12 +779,10 @@ namespace Avalonia.Controls } } - RowDefinitionCollectionDirty = false; + RowDefinitionsDirty = false; } Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Length > 0); - - ExitCounter(Counters._ValidateRowsStructure); } /// @@ -1071,8 +1022,6 @@ namespace Avalonia.Controls int cell, bool forceInfinityV) { - EnterCounter(Counters._MeasureCell); - double cellMeasureWidth; double cellMeasureHeight; @@ -1113,19 +1062,15 @@ namespace Avalonia.Controls PrivateCells[cell].RowSpan); } - EnterCounter(Counters.__MeasureChild); - IControl child = InternalChildren[cell]; + var child = Children[cell]; + if (child != null) { Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); child.Measure(childConstraint); } - ExitCounter(Counters.__MeasureChild); - - ExitCounter(Counters._MeasureCell); } - /// /// Calculates one dimensional measure size for given definitions' range. /// @@ -3032,8 +2977,8 @@ namespace Avalonia.Controls /// private class ExtendedData { - internal ColumnDefinitionCollection ColumnDefinitions; // collection of column definitions (logical tree support) - internal RowDefinitionCollection RowDefinitions; // collection of row definitions (logical tree support) + internal ColumnDefinitions ColumnDefinitions; // collection of column definitions (logical tree support) + internal RowDefinitions RowDefinitions; // collection of row definitions (logical tree support) internal DefinitionBase[] DefinitionsU; // collection of column definitions used during calc internal DefinitionBase[] DefinitionsV; // collection of row definitions used during calc internal CellCache[] CellCachesCollection; // backing store for logical Children @@ -3764,8 +3709,8 @@ namespace Avalonia.Controls { Debug.Assert(grid != null); _currentEnumerator = -1; - _enumerator0 = new ColumnDefinitionCollection.Enumerator(grid.ExtData != null ? grid.ExtData.ColumnDefinitions : null); - _enumerator1 = new RowDefinitionCollection.Enumerator(grid.ExtData != null ? grid.ExtData.RowDefinitions : null); + _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) @@ -3816,8 +3761,8 @@ namespace Avalonia.Controls private int _currentEnumerator; private Object _currentChild; - private ColumnDefinitionCollection.Enumerator _enumerator0; - private RowDefinitionCollection.Enumerator _enumerator1; + private ColumnDefinitions.Enumerator _enumerator0; + private RowDefinitions.Enumerator _enumerator1; private Controls _enumerator2Collection; private int _enumerator2Index; private int _enumerator2Count; From 9f1d70e5926681eb8bc538bc84cbcb55f255e819 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 23 May 2019 18:52:44 +0800 Subject: [PATCH 031/179] Part 4 of n Removed more legacy stuff Added Math Functions needed from WPF's DoubleUtil. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 123 +++ src/Avalonia.Controls/DefinitionBase.cs | 894 ++++++++++++++++++- src/Avalonia.Controls/GridWPF.cs | 468 +--------- 3 files changed, 1031 insertions(+), 454 deletions(-) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index dcb3ef4a2b..2f138d9e83 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.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; + namespace Avalonia.Utilities { /// @@ -8,6 +10,127 @@ namespace Avalonia.Utilities /// public static class MathUtilities { + /// + /// AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. Note that this epsilon is proportional + /// to the numbers themselves to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the AreClose comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// LessThan - Returns whether or not the first double is less than the second double. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. Note that this epsilon is proportional to the numbers themselves + /// to that AreClose survives scalar multiplication. Note, + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the LessThan comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThan(double value1, double value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + + + /// + /// GreaterThan - Returns whether or not the first double is greater than the second double. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. Note that this epsilon is proportional to the numbers themselves + /// to that AreClose survives scalar multiplication. + /// + /// - the result of the GreaterThan comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThan(double value1, double value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + + /// + /// LessThanOrClose - Returns whether or not the first double is less than or close to + /// the second double. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. Note that this epsilon is proportional to the numbers + /// themselves to that AreClose survives scalar multiplication. Note, + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the LessThanOrClose comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThanOrClose(double value1, double value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + + /// + /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to + /// the second double. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first double to compare. + /// The second double to compare. + /// the result of the GreaterThanOrClose comparision. + public static bool GreaterThanOrClose(double value1, double value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + + /// + /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// + /// bool - the result of the AreClose comparision. + /// + /// The double to compare to 1. + public static bool IsOne(double value) + { + return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// + /// bool - the result of the AreClose comparision. + /// + /// The double to compare to 0. + public static bool IsZero(double value) + { + return Math.Abs(value) < 10.0 * double.Epsilon; + } + /// /// Clamps a value between a minimum and maximum value. /// diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 5726356830..05cb9dfb28 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -1,18 +1,24 @@ // 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.Generic; +using System.Diagnostics; + namespace Avalonia.Controls { + /// /// Base class for and . /// public class DefinitionBase : AvaloniaObject { + /// /// Defines the property. /// public static readonly StyledProperty SharedSizeGroupProperty = - AvaloniaProperty.Register(nameof(SharedSizeGroup), inherits: true); + AvaloniaProperty.Register(nameof(SharedSizeGroup), inherits: true, validate: SharedSizeGroupPropertyValueValid); /// /// Gets or sets the name of the shared size group of the column or row. @@ -22,5 +28,889 @@ namespace Avalonia.Controls get { return GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + + internal DefinitionBase(bool isColumnDefinition) + { + _isColumnDefinition = isColumnDefinition; + _parentIndex = -1; + } + + + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + /// + /// Callback to notify about entering model tree. + /// + internal void OnEnterParentTree() + { + 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); + } + } + } + } + + /// + /// 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 static void OnUserSizePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + DefinitionBase definition = (DefinitionBase)d; + + if (definition.InParentLogicalTree) + { + if (definition._sharedState != null) + { + definition._sharedState.Invalidate(); + } + else + { + Grid parentGrid = (Grid)definition.Parent; + + if (((GridLength)e.OldValue).GridUnitType != ((GridLength)e.NewValue).GridUnitType) + { + parentGrid.Invalidate(); + } + else + { + parentGrid.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(DependencyObject d, DependencyPropertyChangedEventArgs 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 (!DoubleUtil.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); + } + + /// + /// + /// + /// + /// This method needs to be internal to be accessable from derived classes. + /// + internal static void OnUserMaxSizePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs 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 (!DoubleUtil.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(DependencyObject d, DependencyPropertyChangedEventArgs 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); + _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 GridLength UserSizeValueCache + { + get + { + return (GridLength)GetValue( + _isColumnDefinition ? + ColumnDefinition.WidthProperty : + RowDefinition.HeightProperty); + } + } + + /// + /// Internal helper to access up-to-date UserMinSize property value. + /// + internal double UserMinSizeValueCache + { + get + { + return (double)GetValue( + _isColumnDefinition ? + ColumnDefinition.MinWidthProperty : + RowDefinition.MinHeightProperty); + } + } + + /// + /// Internal helper to access up-to-date UserMaxSize property value. + /// + internal double UserMaxSizeValueCache + { + get + { + return (double)GetValue( + _isColumnDefinition ? + ColumnDefinition.MaxWidthProperty : + RowDefinition.MaxHeightProperty); + } + } + + /// + /// Protected. Returns true if this DefinitionBase instance is in parent's logical tree. + /// + internal bool InParentLogicalTree + { + get { return (_parentIndex != -1); } + } + + #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(AvaloniaPropertyChangedEventArgs e) + { + DefinitionBase definition = (DefinitionBase)e.Sender; + + 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 bool SharedSizeGroupPropertyValueValid(string value) + { + // null is default value + if (value == null) + { + return (true); + } + + 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 (true); + } + } + + return (false); + } + + /// + /// + /// + /// + /// 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 + private int _parentIndex; // 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 + /// + private 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 + /// + private 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(IControl 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 = !DoubleUtil.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 (!DoubleUtil.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 (!DoubleUtil.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 IControl _layoutUpdatedHost; // IControl 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 + } + + + /// + /// Static ctor. Used for static registration of properties. + /// + static DefinitionBase() + { + SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + } + + #endregion Properties } -} \ No newline at end of file +} + + diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 0c8f571506..96543fda81 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -868,7 +868,7 @@ namespace Avalonia.Controls { for (int i = 0; i < minSizes.Length; i++) { - if (DoubleUtil.GreaterThanOrClose(minSizes[i], 0)) + if (MathUtilities.GreaterThanOrClose(minSizes[i], 0)) { if (isRows) { @@ -926,7 +926,7 @@ namespace Avalonia.Controls MeasureCell(i, forceInfinityV); - hasDesiredSizeUChanged |= !DoubleUtil.AreClose(oldWidth, Children[i].DesiredSize.Width); + hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, Children[i].DesiredSize.Width); if (!ignoreDesiredSizeU) { @@ -1284,10 +1284,10 @@ namespace Avalonia.Controls // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers Debug.Assert(!double.IsInfinity(totalRemainingSize) - && !DoubleUtil.IsNaN(totalRemainingSize) + && !MathUtilities.IsNaN(totalRemainingSize) && totalRemainingSize > 0 && !double.IsInfinity(sizeToDistribute) - && !DoubleUtil.IsNaN(sizeToDistribute) + && !MathUtilities.IsNaN(sizeToDistribute) && sizeToDistribute > 0); for (int i = 0; i < count; ++i) @@ -1320,107 +1320,6 @@ namespace Avalonia.Controls /// /// Must initialize LayoutSize for all Star entries in given array of definitions. /// - private void ResolveStar( - DefinitionBase[] definitions, - double availableSize) - { - if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) - { - ResolveStarLegacy(definitions, availableSize); - } - else - { - ResolveStarMaxDiscrepancy(definitions, availableSize); - } - } - - // original implementation, used from 3.0 through 4.6.2 - private void ResolveStarLegacy( - DefinitionBase[] definitions, - double availableSize) - { - DefinitionBase[] tempDefinitions = TempDefinitions; - int starDefinitionsCount = 0; - double takenSize = 0; - - for (int i = 0; i < definitions.Length; ++i) - { - switch (definitions[i].SizeType) - { - case (LayoutTimeSizeType.Auto): - takenSize += definitions[i].MinSize; - break; - case (LayoutTimeSizeType.Pixel): - takenSize += definitions[i].MeasureSize; - break; - case (LayoutTimeSizeType.Star): - { - tempDefinitions[starDefinitionsCount++] = definitions[i]; - - double starValue = definitions[i].UserSize.Value; - - if (_IsZero(starValue)) - { - definitions[i].MeasureSize = 0; - definitions[i].SizeCache = 0; - } - else - { - // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values - // can be summed up without overflow - starValue = Math.Min(starValue, c_starClip); - - // Note: normalized star value is temporary cached into MeasureSize - definitions[i].MeasureSize = starValue; - double maxSize = Math.Max(definitions[i].MinSize, definitions[i].UserMaxSize); - maxSize = Math.Min(maxSize, c_starClip); - definitions[i].SizeCache = maxSize / starValue; - } - } - break; - } - } - - if (starDefinitionsCount > 0) - { - Array.Sort(tempDefinitions, 0, starDefinitionsCount, s_starDistributionOrderComparer); - - // the 'do {} while' loop below calculates sum of star weights in order to avoid fp overflow... - // partial sum value is stored in each definition's SizeCache member. - // this way the algorithm guarantees (starValue <= definition.SizeCache) and thus - // (starValue / definition.SizeCache) will never overflow due to sum of star weights becoming zero. - // this is an important change from previous implementation where the following was possible: - // ((BigValueStar + SmallValueStar) - BigValueStar) resulting in 0... - double allStarWeights = 0; - int i = starDefinitionsCount - 1; - do - { - allStarWeights += tempDefinitions[i].MeasureSize; - tempDefinitions[i].SizeCache = allStarWeights; - } while (--i >= 0); - - i = 0; - do - { - double resolvedSize; - double starValue = tempDefinitions[i].MeasureSize; - - if (_IsZero(starValue)) - { - resolvedSize = tempDefinitions[i].MinSize; - } - else - { - double userSize = Math.Max(availableSize - takenSize, 0.0) * (starValue / tempDefinitions[i].SizeCache); - resolvedSize = Math.Min(userSize, tempDefinitions[i].UserMaxSize); - resolvedSize = Math.Max(tempDefinitions[i].MinSize, resolvedSize); - } - - tempDefinitions[i].MeasureSize = resolvedSize; - takenSize += resolvedSize; - } while (++i < starDefinitionsCount); - } - } // new implementation as of 4.7. Several improvements: // 1. Allocate to *-defs hitting their min or max constraints, before allocating @@ -1435,7 +1334,8 @@ namespace Avalonia.Controls // 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( + + private void ResolveStar( DefinitionBase[] definitions, double availableSize) { @@ -1755,272 +1655,7 @@ namespace Avalonia.Controls DefinitionBase[] definitions, double finalSize, bool columns) - { - if (FrameworkAppContextSwitches.GridStarDefinitionsCanExceedAvailableSpace) - { - SetFinalSizeLegacy(definitions, finalSize, columns); - } - else - { - SetFinalSizeMaxDiscrepancy(definitions, finalSize, columns); - } - } - - // original implementation, used from 3.0 through 4.6.2 - private void SetFinalSizeLegacy( - DefinitionBase[] definitions, - double finalSize, - bool columns) - { - int starDefinitionsCount = 0; // traverses form the first entry up - int nonStarIndex = definitions.Length; // traverses from the last entry down - double allPreferredArrangeSize = 0; - bool useLayoutRounding = this.UseLayoutRounding; - int[] definitionIndices = DefinitionIndices; - double[] roundingErrors = null; - - // If using layout rounding, check whether rounding needs to compensate for high DPI - double dpi = 1.0; - - if (useLayoutRounding) - { - DpiScale dpiScale = GetDpi(); - dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; - roundingErrors = RoundingErrors; - } - - for (int i = 0; i < definitions.Length; ++i) - { - // if definition is shared then is cannot be star - Debug.Assert(!definitions[i].IsShared || !definitions[i].UserSize.IsStar); - - if (definitions[i].UserSize.IsStar) - { - double starValue = definitions[i].UserSize.Value; - - if (_IsZero(starValue)) - { - // cach normilized star value temporary into MeasureSize - definitions[i].MeasureSize = 0; - definitions[i].SizeCache = 0; - } - else - { - // clipping by c_starClip guarantees that sum of even a very big number of max'ed out star values - // can be summed up without overflow - starValue = Math.Min(starValue, c_starClip); - - // Note: normalized star value is temporary cached into MeasureSize - definitions[i].MeasureSize = starValue; - double maxSize = Math.Max(definitions[i].MinSizeForArrange, definitions[i].UserMaxSize); - maxSize = Math.Min(maxSize, c_starClip); - definitions[i].SizeCache = maxSize / starValue; - if (useLayoutRounding) - { - roundingErrors[i] = definitions[i].SizeCache; - definitions[i].SizeCache = IControl.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 = IControl.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 = IControl.RoundLayoutValue(definitions[definitionIndices[i]].SizeCache, dpi); - } - - allPreferredArrangeSize += definitions[definitionIndices[i]].SizeCache; - } while (++i < starDefinitionsCount); - } - - if (allPreferredArrangeSize > finalSize - && !_AreClose(allPreferredArrangeSize, finalSize)) - { - DistributionOrderIndexComparer distributionOrderIndexComparer = new DistributionOrderIndexComparer(definitions); - Array.Sort(definitionIndices, 0, definitions.Length, distributionOrderIndexComparer); - double sizeToDistribute = finalSize - allPreferredArrangeSize; - - for (int i = 0; i < definitions.Length; ++i) - { - int definitionIndex = definitionIndices[i]; - double final = definitions[definitionIndex].SizeCache + (sizeToDistribute / (definitions.Length - i)); - double finalOld = final; - final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); - final = Math.Min(final, definitions[definitionIndex].SizeCache); - - if (useLayoutRounding) - { - roundingErrors[definitionIndex] = final; - final = IControl.RoundLayoutValue(finalOld, dpi); - final = Math.Max(final, definitions[definitionIndex].MinSizeForArrange); - final = Math.Min(final, definitions[definitionIndex].SizeCache); - } - - sizeToDistribute -= (final - definitions[definitionIndex].SizeCache); - definitions[definitionIndex].SizeCache = final; - } - - allPreferredArrangeSize = finalSize - sizeToDistribute; - } - - if (useLayoutRounding) - { - if (!_AreClose(allPreferredArrangeSize, finalSize)) - { - // Compute deltas - for (int i = 0; i < definitions.Length; ++i) - { - roundingErrors[i] = roundingErrors[i] - definitions[i].SizeCache; - definitionIndices[i] = i; - } - - // Sort rounding errors - RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); - Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); - double adjustedSize = allPreferredArrangeSize; - double dpiIncrement = IControl.RoundLayoutValue(1.0, dpi); - - if (allPreferredArrangeSize > finalSize) - { - int i = definitions.Length - 1; - while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache - dpiIncrement; - final = Math.Max(final, definition.MinSizeForArrange); - if (final < definition.SizeCache) - { - adjustedSize -= dpiIncrement; - } - definition.SizeCache = final; - i--; - } - } - else if (allPreferredArrangeSize < finalSize) - { - int i = 0; - while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Length) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache + dpiIncrement; - final = Math.Max(final, definition.MinSizeForArrange); - if (final > definition.SizeCache) - { - adjustedSize += dpiIncrement; - } - definition.SizeCache = final; - i++; - } - } - } - } - - definitions[0].FinalOffset = 0.0; - for (int i = 0; i < definitions.Length; ++i) - { - definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; - } - } - - // new implementation, as of 4.7. This incorporates the same algorithm - // as in ResolveStarMaxDiscrepancy. It differs in the same way that SetFinalSizeLegacy - // differs from ResolveStarLegacy, namely (a) leaves results in def.SizeCache - // instead of def.MeasureSize, (b) implements LayoutRounding if requested, - // (c) stores intermediate results differently. - // The LayoutRounding logic is improved: - // 1. Use pre-rounded values during proportional allocation. This avoids the - // same kind of problems arising from interaction with min/max that - // motivated the new algorithm in the first place. - // 2. Use correct "nudge" amount when distributing roundoff space. This - // comes into play at high DPI - greater than 134. - // 3. Applies rounding only to real pixel values (not to ratios) - private void SetFinalSizeMaxDiscrepancy( - DefinitionBase[] definitions, - double finalSize, - bool columns) - { + { int defCount = definitions.Length; int[] definitionIndices = DefinitionIndices; int minCount = 0, maxCount = 0; @@ -2359,7 +1994,7 @@ namespace Avalonia.Controls for (int i = 0; i < definitions.Length; ++i) { DefinitionBase def = definitions[i]; - double roundedSize = IControl.RoundLayoutValue(def.SizeCache, dpi); + double roundedSize = Control.RoundLayoutValue(def.SizeCache, dpi); roundingErrors[i] = (roundedSize - def.SizeCache); def.SizeCache = roundedSize; roundedTakenSize += roundedSize; @@ -2561,9 +2196,6 @@ namespace Avalonia.Controls 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" @@ -3403,7 +3035,7 @@ namespace Avalonia.Controls internal StarDistributionOrderIndexComparer(DefinitionBase[] definitions) { - Invariant.Assert(definitions != null); + Contract.Requires(definitions != null); this.definitions = definitions; } @@ -3444,7 +3076,7 @@ namespace Avalonia.Controls internal DistributionOrderIndexComparer(DefinitionBase[] definitions) { - Invariant.Assert(definitions != null); + Contract.Requires(definitions != null); this.definitions = definitions; } @@ -3487,7 +3119,7 @@ namespace Avalonia.Controls internal RoundingErrorIndexComparer(double[] errors) { - Invariant.Assert(errors != null); + Contract.Requires(errors != null); this.errors = errors; } @@ -3586,7 +3218,7 @@ namespace Avalonia.Controls internal MinRatioIndexComparer(DefinitionBase[] definitions) { - Invariant.Assert(definitions != null); + Contract.Requires(definitions != null); this.definitions = definitions; } @@ -3627,7 +3259,7 @@ namespace Avalonia.Controls internal MaxRatioIndexComparer(DefinitionBase[] definitions) { - Invariant.Assert(definitions != null); + Contract.Requires(definitions != null); this.definitions = definitions; } @@ -3668,7 +3300,7 @@ namespace Avalonia.Controls internal StarWeightIndexComparer(DefinitionBase[] definitions) { - Invariant.Assert(definitions != null); + Contract.Requires(definitions != null); this.definitions = definitions; } @@ -3699,79 +3331,11 @@ namespace Avalonia.Controls return result; } } - - /// - /// Implementation of a simple enumerator of grid's logical Children - /// - private class GridChildrenCollectionEnumeratorSimple : IEnumerator - { - internal GridChildrenCollectionEnumeratorSimple(Grid grid, bool includeChildren) - { - Debug.Assert(grid != null); - _currentEnumerator = -1; - _enumerator0 = new 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; - } - } - - 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 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; - } - + /// /// Helper to render grid lines. /// - internal class GridLinesRenderer : DrawingVisual + internal class GridLinesRenderer : Visual { /// /// Static initialization From 9e052c5acca28ebe16ec0fd0afd4fb8708ad0d07 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 24 May 2019 12:38:20 +0800 Subject: [PATCH 032/179] Fix misplaced compile command on proj. --- src/Avalonia.Controls/Avalonia.Controls.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index eabe136791..c321d3a2f1 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -11,8 +11,8 @@ + - From d7b3ebb9f788e31c718977511e003f0c58adf6d2 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 24 May 2019 13:23:23 +0800 Subject: [PATCH 033/179] Part 5 of n --- src/Avalonia.Controls/DefinitionBase.cs | 11 +- src/Avalonia.Controls/GridWPF.cs | 430 ++++-------------------- 2 files changed, 63 insertions(+), 378 deletions(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 05cb9dfb28..10d6b55285 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -186,7 +187,7 @@ namespace Avalonia.Controls internal static bool IsUserMinSizePropertyValueValid(object value) { double v = (double)value; - return (!DoubleUtil.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); + return (!MathUtilities.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); } /// @@ -215,7 +216,7 @@ namespace Avalonia.Controls internal static bool IsUserMaxSizePropertyValueValid(object value) { double v = (double)value; - return (!DoubleUtil.IsNaN(v) && v >= 0.0d); + return (!MathUtilities.IsNaN(v) && v >= 0.0d); } /// @@ -847,7 +848,7 @@ namespace Avalonia.Controls sharedMinSize = Math.Max(sharedMinSize, _registry[i].MinSize); } - bool sharedMinSizeChanged = !DoubleUtil.AreClose(_minSize, sharedMinSize); + 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) @@ -857,7 +858,7 @@ namespace Avalonia.Controls if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated) { // if definition's min size is different, then need to re-measure - if (!DoubleUtil.AreClose(sharedMinSize, definitionBase.MinSize)) + if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize)) { Grid parentGrid = (Grid)definitionBase.Parent; parentGrid.InvalidateMeasure(); @@ -870,7 +871,7 @@ namespace Avalonia.Controls // 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 (!DoubleUtil.AreClose(sharedMinSize, definitionBase.SizeCache)) + if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache)) { Grid parentGrid = (Grid)definitionBase.Parent; parentGrid.InvalidateArrange(); diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 96543fda81..30ca751e57 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -16,6 +16,7 @@ using Avalonia.Controls; using Avalonia.Media; using Avalonia; using System.Collections; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -153,7 +154,7 @@ namespace Avalonia.Controls { child.Measure(constraint); gridDesiredSize = new Size( - Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); } } @@ -199,156 +200,6 @@ namespace Avalonia.Controls Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); - // Grid classifies cells into four groups depending on - // the column / row type a cell belongs to (number corresponds to - // group number): - // - // Px Auto Star - // +--------+--------+--------+ - // | | | | - // Px | 1 | 1 | 3 | - // | | | | - // +--------+--------+--------+ - // | | | | - // Auto | 1 | 1 | 3 | - // | | | | - // +--------+--------+--------+ - // | | | | - // Star | 4 | 2 | 4 | - // | | | | - // +--------+--------+--------+ - // - // The group number indicates the order in which cells are measured. - // Certain order is necessary to be able to dynamically resolve star - // columns / rows sizes which are used as input for measuring of - // the cells belonging to them. - // - // However, there are cases when topology of a grid causes cyclical - // size dependences. For example: - // - // - // column width="Auto" column width="*" - // +----------------------+----------------------+ - // | | | - // | | | - // | | | - // | | | - // row height="Auto" | | cell 1 2 | - // | | | - // | | | - // | | | - // | | | - // +----------------------+----------------------+ - // | | | - // | | | - // | | | - // | | | - // row height="*" | cell 2 1 | | - // | | | - // | | | - // | | | - // | | | - // +----------------------+----------------------+ - // - // In order to accurately calculate constraint width for "cell 1 2" - // (which is the remaining of grid's available width and calculated - // value of Auto column), "cell 2 1" needs to be calculated first, - // as it contributes to the Auto column's calculated value. - // At the same time in order to accurately calculate constraint - // height for "cell 2 1", "cell 1 2" needs to be calcualted first, - // as it contributes to Auto row height, which is used in the - // computation of Star row resolved height. - // - // to "break" this cyclical dependency we are making (arbitrary) - // decision to treat cells like "cell 2 1" as if they appear in Auto - // rows. And then recalculate them one more time when star row - // heights are resolved. - // - // (Or more strictly) the code below implement the following logic: - // - // +---------+ - // | enter | - // +---------+ - // | - // V - // +----------------+ - // | Measure Group1 | - // +----------------+ - // | - // V - // / - \ - // / \ - // Y / Can \ N - // +--------| Resolve |-----------+ - // | \ StarsV? / | - // | \ / | - // | \ - / | - // V V - // +----------------+ / - \ - // | Resolve StarsV | / \ - // +----------------+ Y / Can \ N - // | +----| Resolve |------+ - // V | \ StarsU? / | - // +----------------+ | \ / | - // | Measure Group2 | | \ - / | - // +----------------+ | V - // | | +-----------------+ - // V | | Measure Group2' | - // +----------------+ | +-----------------+ - // | Resolve StarsU | | | - // +----------------+ V V - // | +----------------+ +----------------+ - // V | Resolve StarsU | | Resolve StarsU | - // +----------------+ +----------------+ +----------------+ - // | Measure Group3 | | | - // +----------------+ V V - // | +----------------+ +----------------+ - // | | Measure Group3 | | Measure Group3 | - // | +----------------+ +----------------+ - // | | | - // | V V - // | +----------------+ +----------------+ - // | | Resolve StarsV | | Resolve StarsV | - // | +----------------+ +----------------+ - // | | | - // | | V - // | | +------------------+ - // | | | Measure Group2'' | - // | | +------------------+ - // | | | - // +----------------------+-------------------------+ - // | - // V - // +----------------+ - // | Measure Group4 | - // +----------------+ - // | - // V - // +--------+ - // | exit | - // +--------+ - // - // where: - // * all [Measure GroupN] - regular Children measure process - - // each cell is measured given contraint size as an input - // and each cell's desired size is accumulated on the - // corresponding column / row; - // * [Measure Group2'] - is when each cell is measured with - // infinit height as a constraint and a cell's desired - // height is ignored; - // * [Measure Groups''] - is when each cell is measured (second - // time during single Grid.MeasureOverride) regularly but its - // returned width is ignored; - // - // This algorithm is believed to be as close to ideal as possible. - // It has the following drawbacks: - // * cells belonging to Group2 can be called to measure twice; - // * iff during second measure a cell belonging to Group2 returns - // desired width greater than desired width returned the first - // time, such a cell is going to be clipped, even though it - // appears in Auto column. - // - MeasureCellsGroup(extData.CellGroup1, constraint, false, false); { @@ -478,7 +329,7 @@ namespace Avalonia.Controls } // update render bound on grid lines renderer visual - GridLinesRenderer gridLinesRenderer = EnsureGridLinesRenderer(); + var gridLinesRenderer = EnsureGridLinesRenderer(); if (gridLinesRenderer != null) { gridLinesRenderer.UpdateRenderBounds(arrangeSize); @@ -624,23 +475,23 @@ namespace Avalonia.Controls // // 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(Grid.ColumnProperty (child), DefinitionsU.Length - 1); + cell.ColumnIndex = Math.Min(child.GetValue(Grid.ColumnProperty), DefinitionsU.Length - 1); // clamp to value < number_of_rows // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Length - 1); + cell.RowIndex = Math.Min(child.GetValue(Grid.RowProperty), DefinitionsV.Length - 1); // read span properties // clamp to not exceed beyond right side of the grid // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(GetColumnSpan(child), DefinitionsU.Length - cell.ColumnIndex); + cell.ColumnSpan = Math.Min(child.GetValue(Grid.ColumnSpanProperty), DefinitionsU.Length - cell.ColumnIndex); // clamp to not exceed beyond bottom side of the grid // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Length - cell.RowIndex); + cell.RowSpan = Math.Min(child.GetValue(Grid.RowSpanProperty), DefinitionsV.Length - cell.RowIndex); Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Length); Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Length); @@ -1063,7 +914,7 @@ namespace Avalonia.Controls } var child = Children[cell]; - + if (child != null) { Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); @@ -1334,7 +1185,7 @@ namespace Avalonia.Controls // 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 ResolveStar( DefinitionBase[] definitions, double availableSize) @@ -1655,7 +1506,7 @@ namespace Avalonia.Controls DefinitionBase[] definitions, double finalSize, bool columns) - { + { int defCount = definitions.Length; int[] definitionIndices = DefinitionIndices; int minCount = 0, maxCount = 0; @@ -2219,12 +2070,12 @@ namespace Avalonia.Controls if (ShowGridLines && (_gridLinesRenderer == null)) { _gridLinesRenderer = new GridLinesRenderer(); - this.AddVisualChild(_gridLinesRenderer); + this.VisualChildren.Add(_gridLinesRenderer); } if ((!ShowGridLines) && (_gridLinesRenderer != null)) { - this.RemoveVisualChild(_gridLinesRenderer); + this.VisualChildren.Remove(_gridLinesRenderer); _gridLinesRenderer = null; } @@ -2287,7 +2138,7 @@ namespace Avalonia.Controls if (child != null) { - Grid grid = VisualTreeHelper.GetParent(child) as Grid; + Grid grid = child.GetVisualParent() as Grid; if (grid != null && grid.ExtData != null && grid.ListenToNotifications) @@ -2514,7 +2365,7 @@ namespace Avalonia.Controls /// true if d == 0. private static bool _IsZero(double d) { - return (Math.Abs(d) < c_epsilon); + return (Math.Abs(d) < double.Epsilon); } /// @@ -2525,7 +2376,7 @@ namespace Avalonia.Controls /// true if d1 == d2 private static bool _AreClose(double d1, double d2) { - return (Math.Abs(d1 - d2) < c_epsilon); + return (Math.Abs(d1 - d2) < double.Epsilon); } /// @@ -2554,15 +2405,6 @@ namespace Avalonia.Controls } } - #endregion Private Properties - - //------------------------------------------------------ - // - // Private Fields - // - //------------------------------------------------------ - - #region Private Fields private ExtendedData _data; // extended data instantiated on demand, for non-trivial case handling only private Flags _flags; // grid validity / property caches dirtiness flags private GridLinesRenderer _gridLinesRenderer; @@ -2573,15 +2415,6 @@ namespace Avalonia.Controls // Stores unrounded values and rounding errors during layout rounding. double[] _roundingErrors; - #endregion Private Fields - - //------------------------------------------------------ - // - // Static Fields - // - //------------------------------------------------------ - - #region Static Fields private const double c_epsilon = 1e-5; // used in fp calculations private const double c_starClip = 1e298; // used as maximum for clipping star values during normalization private const int c_layoutLoopMaxCount = 5; // 5 is an arbitrary constant chosen to end the measure loop @@ -2594,16 +2427,6 @@ namespace Avalonia.Controls private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); - #endregion Static Fields - - //------------------------------------------------------ - // - // Private Structures / Classes - // - //------------------------------------------------------ - - #region Private Structures Classes - /// /// Extended data instantiated on demand, when grid handles non-trivial case. /// @@ -2656,140 +2479,6 @@ namespace Avalonia.Controls ArrangeOverrideInProgress = 0x00080000, // "1" while in the context of Grid.ArrangeOverride } - #endregion Private Structures Classes - - //------------------------------------------------------ - // - // Properties - // - //------------------------------------------------------ - - #region Properties - - /// - /// ShowGridLines property. This property is used mostly - /// for simplification of visual debuggig. When it is set - /// to true grid lines are drawn to visualize location - /// of grid lines. - /// - public static readonly DependencyProperty ShowGridLinesProperty = - DependencyProperty.Register( - "ShowGridLines", - typeof(bool), - typeof(Grid), - new FrameworkPropertyMetadata( - false, - new PropertyChangedCallback(OnShowGridLinesPropertyChanged))); - - /// - /// Column property. This is an attached property. - /// Grid defines Column property, so that it can be set - /// on any element treated as a cell. Column property - /// specifies child's position with respect to columns. - /// - /// - /// Columns are 0 - based. In order to appear in first column, element - /// should have Column property set to 0. - /// Default value for the property is 0. - /// - [CommonDependencyProperty] - public static readonly DependencyProperty ColumnProperty = - DependencyProperty.RegisterAttached( - "Column", - typeof(int), - typeof(Grid), - new FrameworkPropertyMetadata( - 0, - new PropertyChangedCallback(OnCellAttachedPropertyChanged)), - new ValidateValueCallback(IsIntValueNotNegative)); - - /// - /// Row property. This is an attached property. - /// Grid defines Row, so that it can be set - /// on any element treated as a cell. Row property - /// specifies child's position with respect to rows. - /// - /// Rows are 0 - based. In order to appear in first row, element - /// should have Row property set to 0. - /// Default value for the property is 0. - /// - /// - [CommonDependencyProperty] - public static readonly DependencyProperty RowProperty = - DependencyProperty.RegisterAttached( - "Row", - typeof(int), - typeof(Grid), - new FrameworkPropertyMetadata( - 0, - new PropertyChangedCallback(OnCellAttachedPropertyChanged)), - new ValidateValueCallback(IsIntValueNotNegative)); - - /// - /// ColumnSpan property. This is an attached property. - /// Grid defines ColumnSpan, so that it can be set - /// on any element treated as a cell. ColumnSpan property - /// specifies child's width with respect to columns. - /// Example, ColumnSpan == 2 means that child will span across two columns. - /// - /// - /// Default value for the property is 1. - /// - [CommonDependencyProperty] - public static readonly DependencyProperty ColumnSpanProperty = - DependencyProperty.RegisterAttached( - "ColumnSpan", - typeof(int), - typeof(Grid), - new FrameworkPropertyMetadata( - 1, - new PropertyChangedCallback(OnCellAttachedPropertyChanged)), - new ValidateValueCallback(IsIntValueGreaterThanZero)); - - /// - /// RowSpan property. This is an attached property. - /// Grid defines RowSpan, so that it can be set - /// on any element treated as a cell. RowSpan property - /// specifies child's height with respect to row grid lines. - /// Example, RowSpan == 3 means that child will span across three rows. - /// - /// - /// Default value for the property is 1. - /// - [CommonDependencyProperty] - public static readonly DependencyProperty RowSpanProperty = - DependencyProperty.RegisterAttached( - "RowSpan", - typeof(int), - typeof(Grid), - new FrameworkPropertyMetadata( - 1, - new PropertyChangedCallback(OnCellAttachedPropertyChanged)), - new ValidateValueCallback(IsIntValueGreaterThanZero)); - - - /// - /// IsSharedSizeScope property marks scoping element for shared size. - /// - public static readonly DependencyProperty IsSharedSizeScopeProperty = - DependencyProperty.RegisterAttached( - "IsSharedSizeScope", - typeof(bool), - typeof(Grid), - new FrameworkPropertyMetadata( - false, - new PropertyChangedCallback(DefinitionBase.OnIsSharedSizeScopePropertyChanged))); - - #endregion Properties - - //------------------------------------------------------ - // - // Internal Structures / Classes - // - //------------------------------------------------------ - - #region Internal Structures Classes - /// /// LayoutTimeSizeType is used internally and reflects layout-time size type. /// @@ -2802,16 +2491,6 @@ namespace Avalonia.Controls Star = 0x04, } - #endregion Internal Structures Classes - - //------------------------------------------------------ - // - // Private Structures / Classes - // - //------------------------------------------------------ - - #region Private Structures Classes - /// /// CellCache stored calculated values of /// 1. attached cell positioning properties; @@ -3331,7 +3010,7 @@ namespace Avalonia.Controls return result; } } - + /// /// Helper to render grid lines. /// @@ -3342,53 +3021,52 @@ namespace Avalonia.Controls /// static GridLinesRenderer() { - s_oddDashPen = new Pen(Brushes.Blue, c_penWidth); - DoubleCollection oddDashArray = new DoubleCollection(); + var oddDashArray = new List(); oddDashArray.Add(c_dashLength); oddDashArray.Add(c_dashLength); - s_oddDashPen.DashStyle = new DashStyle(oddDashArray, 0); - s_oddDashPen.DashCap = PenLineCap.Flat; - s_oddDashPen.Freeze(); + var ds1 = new DashStyle(oddDashArray, 0); + s_oddDashPen = new Pen(Brushes.Blue, + c_penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); - s_evenDashPen = new Pen(Brushes.Yellow, c_penWidth); - DoubleCollection evenDashArray = new DoubleCollection(); + var evenDashArray = new List(); evenDashArray.Add(c_dashLength); evenDashArray.Add(c_dashLength); - s_evenDashPen.DashStyle = new DashStyle(evenDashArray, c_dashLength); - s_evenDashPen.DashCap = PenLineCap.Flat; - s_evenDashPen.Freeze(); + var ds2 = new DashStyle(evenDashArray, 0); + s_evenDashPen = new Pen(Brushes.Yellow, + c_penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); } /// /// UpdateRenderBounds. /// - /// Size of render bounds - internal void UpdateRenderBounds(Size boundsSize) + public override void Render(DrawingContext drawingContext) { - using (DrawingContext drawingContext = RenderOpen()) + var grid = this.GetVisualParent() as Grid; + + if (grid == null + || grid.ShowGridLines == false) { - Grid grid = VisualTreeHelper.GetParent(this) as Grid; - if (grid == null - || grid.ShowGridLines == false) - { - return; - } + return; + } - for (int i = 1; i < grid.DefinitionsU.Length; ++i) - { - DrawGridLine( - drawingContext, - grid.DefinitionsU[i].FinalOffset, 0.0, - grid.DefinitionsU[i].FinalOffset, boundsSize.Height); - } + for (int i = 1; i < grid.DefinitionsU.Length; ++i) + { + DrawGridLine( + drawingContext, + grid.DefinitionsU[i].FinalOffset, 0.0, + grid.DefinitionsU[i].FinalOffset, lastArrangeSize.Height); + } - for (int i = 1; i < grid.DefinitionsV.Length; ++i) - { - DrawGridLine( - drawingContext, - 0.0, grid.DefinitionsV[i].FinalOffset, - boundsSize.Width, grid.DefinitionsV[i].FinalOffset); - } + for (int i = 1; i < grid.DefinitionsV.Length; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.DefinitionsV[i].FinalOffset, + lastArrangeSize.Width, grid.DefinitionsV[i].FinalOffset); } } @@ -3402,12 +3080,18 @@ namespace Avalonia.Controls double endX, double endY) { - Point start = new Point(startX, startY); - Point end = new Point(endX, endY); + var start = new Point(startX, startY); + var end = new Point(endX, endY); drawingContext.DrawLine(s_oddDashPen, start, end); drawingContext.DrawLine(s_evenDashPen, start, end); } + internal void UpdateRenderBounds(Size arrangeSize) + { + lastArrangeSize = arrangeSize; + this.InvalidateVisual(); + } + private static Size lastArrangeSize; private const double c_dashLength = 4.0; // private const double c_penWidth = 1.0; // private static readonly Pen s_oddDashPen; // first pen to draw dash From aea3bbcbd026ad72698a79cdeba260702f1cf55d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 24 May 2019 14:12:44 +0800 Subject: [PATCH 034/179] Part 6 of n --- src/Avalonia.Base/Utilities/MathUtilities.cs | 52 ++----------- src/Avalonia.Controls/DefinitionBase.cs | 8 +- src/Avalonia.Controls/GridWPF.cs | 81 +++++++++++--------- 3 files changed, 54 insertions(+), 87 deletions(-) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 2f138d9e83..dc47584f32 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.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.Runtime.InteropServices; namespace Avalonia.Utilities { @@ -12,17 +13,8 @@ namespace Avalonia.Utilities { /// /// AreClose - Returns whether or not two doubles are "close". That is, whether or - /// not they are within epsilon of each other. Note that this epsilon is proportional - /// to the numbers themselves to that AreClose survives scalar multiplication. - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the AreClose comparision. - /// + /// not they are within epsilon of each other. + /// /// The first double to compare. /// The second double to compare. public static bool AreClose(double value1, double value2) @@ -37,17 +29,7 @@ namespace Avalonia.Utilities /// /// LessThan - Returns whether or not the first double is less than the second double. /// That is, whether or not the first is strictly less than *and* not within epsilon of - /// the other number. Note that this epsilon is proportional to the numbers themselves - /// to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the LessThan comparision. - /// + /// the other number. /// The first double to compare. /// The second double to compare. public static bool LessThan(double value1, double value2) @@ -55,15 +37,10 @@ namespace Avalonia.Utilities return (value1 < value2) && !AreClose(value1, value2); } - /// /// GreaterThan - Returns whether or not the first double is greater than the second double. /// That is, whether or not the first is strictly greater than *and* not within epsilon of - /// the other number. Note that this epsilon is proportional to the numbers themselves - /// to that AreClose survives scalar multiplication. - /// - /// - the result of the GreaterThan comparision. - /// + /// the other number. /// The first double to compare. /// The second double to compare. public static bool GreaterThan(double value1, double value2) @@ -74,17 +51,7 @@ namespace Avalonia.Utilities /// /// LessThanOrClose - Returns whether or not the first double is less than or close to /// the second double. That is, whether or not the first is strictly less than or within - /// epsilon of the other number. Note that this epsilon is proportional to the numbers - /// themselves to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the LessThanOrClose comparision. - /// + /// epsilon of the other number. /// The first double to compare. /// The second double to compare. public static bool LessThanOrClose(double value1, double value2) @@ -99,7 +66,6 @@ namespace Avalonia.Utilities /// /// The first double to compare. /// The second double to compare. - /// the result of the GreaterThanOrClose comparision. public static bool GreaterThanOrClose(double value1, double value2) { return (value1 > value2) || AreClose(value1, value2); @@ -109,9 +75,6 @@ namespace Avalonia.Utilities /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), /// but this is faster. /// - /// - /// bool - the result of the AreClose comparision. - /// /// The double to compare to 1. public static bool IsOne(double value) { @@ -122,9 +85,6 @@ namespace Avalonia.Utilities /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), /// but this is faster. /// - /// - /// bool - the result of the AreClose comparision. - /// /// The double to compare to 0. public static bool IsZero(double value) { diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 10d6b55285..e1c5979476 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls /// /// Base class for and . /// - public class DefinitionBase : AvaloniaObject + public class DefinitionBase : ContentControl { /// @@ -307,7 +307,7 @@ namespace Avalonia.Controls /// /// Layout-time user size type. /// - internal Grid.LayoutTimeSizeType SizeType + internal Grid.GridLayoutTimeSizeType SizeType { get { return (_sizeType); } set { _sizeType = value; } @@ -333,7 +333,7 @@ namespace Avalonia.Controls get { double preferredSize = MinSize; - if (_sizeType != Grid.LayoutTimeSizeType.Auto + if (_sizeType != Grid.GridLayoutTimeSizeType.Auto && preferredSize < _measureSize) { preferredSize = _measureSize; @@ -635,7 +635,7 @@ namespace Avalonia.Controls private Flags _flags; // flags reflecting various aspects of internal state private int _parentIndex; // 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 Grid.GridLayoutTimeSizeType _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 diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 30ca751e57..5364f99dae 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -56,6 +56,14 @@ namespace Avalonia.Controls public static readonly AttachedProperty IsSharedSizeScopeProperty = AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowGridLinesProperty = + AvaloniaProperty.Register( + nameof(ShowGridLines), + defaultValue: false); + /// /// ShowGridLines property. /// @@ -64,6 +72,7 @@ namespace Avalonia.Controls get { return (CheckFlagsAnd(Flags.ShowGridLinesPropertyValue)); } set { SetValue(ShowGridLinesProperty, value); } } + private ColumnDefinitions _columnDefinitions; private RowDefinitions _rowDefinitions; @@ -1135,10 +1144,10 @@ namespace Avalonia.Controls // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers Debug.Assert(!double.IsInfinity(totalRemainingSize) - && !MathUtilities.IsNaN(totalRemainingSize) + && !double.IsNaN(totalRemainingSize) && totalRemainingSize > 0 && !double.IsInfinity(sizeToDistribute) - && !MathUtilities.IsNaN(sizeToDistribute) + && !double.IsNaN(sizeToDistribute) && sizeToDistribute > 0); for (int i = 0; i < count; ++i) @@ -1163,15 +1172,6 @@ namespace Avalonia.Controls } } - /// - /// 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. - /// - // 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 @@ -1186,6 +1186,14 @@ namespace Avalonia.Controls // change in available space resulting in large change to one def's allocation. // 3. Correct handling of large *-values, including Infinity. + /// + /// Resolves Star's for given array of definitions. + /// + /// Array of definitions to resolve stars. + /// All available size. + /// + /// Must initialize LayoutSize for all Star entries in given array of definitions. + /// private void ResolveStar( DefinitionBase[] definitions, double availableSize) @@ -2113,12 +2121,30 @@ namespace Avalonia.Controls return (flags == 0 || (_flags & flags) != 0); } - /// - /// - /// - private static void OnShowGridLinesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + + private static int ValidateColumn(AvaloniaObject o, int value) { - Grid grid = (Grid)d; + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Column value."); + } + + return value; + } + + private static int ValidateRow(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Row value."); + } + + return value; + } + + private static void OnShowGridLinesPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + var grid = e.Sender as Grid; if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) @@ -2129,12 +2155,9 @@ namespace Avalonia.Controls grid.SetFlags((bool)e.NewValue, Flags.ShowGridLinesPropertyValue); } - /// - /// - /// - private static void OnCellAttachedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private static void OnCellAttachedPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - Visual child = d as Visual; + var child = e.Sender as Visual; if (child != null) { @@ -2149,22 +2172,6 @@ namespace Avalonia.Controls } } - /// - /// - /// - private static bool IsIntValueNotNegative(object value) - { - return ((int)value >= 0); - } - - /// - /// - /// - private static bool IsIntValueGreaterThanZero(object value) - { - return ((int)value > 0); - } - /// /// Helper for Comparer methods. /// From 645e3131cc21ef59ee86eb4ec3d1f3e37430f5fa Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 24 May 2019 15:20:45 +0800 Subject: [PATCH 035/179] Part 7 of n --- src/Avalonia.Controls/GridWPF.cs | 63 ++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 5364f99dae..d42d6fc651 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -17,6 +17,7 @@ using Avalonia.Media; using Avalonia; using System.Collections; using Avalonia.Utilities; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -25,6 +26,16 @@ namespace Avalonia.Controls /// public class Grid : Panel { + + static Grid() + { + ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + } + /// /// Defines the Column attached property. /// @@ -582,9 +593,9 @@ namespace Avalonia.Controls } else { - extData.ColumnDefinitions.InternalTrimToSize(); + // extData.ColumnDefinitions.InternalTrimToSize(); - if (extData.ColumnDefinitions.InternalCount == 0) + if (extData.ColumnDefinitions.Count == 0) { // if column definitions collection is empty // mockup array with one column @@ -592,7 +603,7 @@ namespace Avalonia.Controls } else { - extData.DefinitionsU = extData.ColumnDefinitions.InternalItems; + extData.DefinitionsU = extData.ColumnDefinitions.ToArray(); } } @@ -625,9 +636,9 @@ namespace Avalonia.Controls } else { - extData.RowDefinitions.InternalTrimToSize(); + // extData.RowDefinitions.InternalTrimToSize(); - if (extData.RowDefinitions.InternalCount == 0) + if (extData.RowDefinitions.Count == 0) { // if row definitions collection is empty // mockup array with one row @@ -635,7 +646,7 @@ namespace Avalonia.Controls } else { - extData.DefinitionsV = extData.RowDefinitions.InternalItems; + extData.DefinitionsV = extData.RowDefinitions.ToArray(); } } @@ -1844,8 +1855,8 @@ namespace Avalonia.Controls // 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 ILayoutRoot)?.LayoutScaling ?? 96; + double[] roundingErrors = RoundingErrors; double roundedTakenSize = 0.0; @@ -1853,7 +1864,7 @@ namespace Avalonia.Controls for (int i = 0; i < definitions.Length; ++i) { DefinitionBase def = definitions[i]; - double roundedSize = Control.RoundLayoutValue(def.SizeCache, dpi); + double roundedSize = RoundLayoutValue(def.SizeCache, dpi); roundingErrors[i] = (roundedSize - def.SizeCache); def.SizeCache = roundedSize; roundedTakenSize += roundedSize; @@ -2099,6 +2110,30 @@ namespace Avalonia.Controls _flags = value ? (_flags | flags) : (_flags & (~flags)); } + private double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!MathUtilities.AreClose(dpiScale, 1.0)) + { + newValue = Math.Round(value * dpiScale) / dpiScale; + // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. + if (double.IsNaN(newValue) || + double.IsInfinity(newValue) || + MathUtilities.AreClose(newValue, Double.MaxValue)) + { + newValue = value; + } + } + else + { + newValue = Math.Round(value); + } + + return newValue; + } + /// /// CheckFlagsAnd returns true if all the flags in the /// given bitmask are set on the object. @@ -2142,10 +2177,8 @@ namespace Avalonia.Controls return value; } - private static void OnShowGridLinesPropertyChanged(AvaloniaPropertyChangedEventArgs e) + private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) { - var grid = e.Sender as Grid; - if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) { @@ -2155,13 +2188,11 @@ namespace Avalonia.Controls grid.SetFlags((bool)e.NewValue, Flags.ShowGridLinesPropertyValue); } - private static void OnCellAttachedPropertyChanged(AvaloniaPropertyChangedEventArgs e) + private static void OnCellAttachedPropertyChanged(Visual child, AvaloniaPropertyChangedEventArgs e) { - var child = e.Sender as Visual; - if (child != null) { - Grid grid = child.GetVisualParent() as Grid; + var grid = child.GetVisualParent() as Grid; if (grid != null && grid.ExtData != null && grid.ListenToNotifications) From 58b6399a52eedc5005b552565dc9620066ebe4d3 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 24 May 2019 15:37:28 +0800 Subject: [PATCH 036/179] Part 8 of preliminary error-plugging, Grid running but somewhat broken. --- src/Avalonia.Controls/ColumnDefinition.cs | 6 +- src/Avalonia.Controls/DefinitionBase.cs | 117 +++++++++--------- src/Avalonia.Controls/GridWPF.cs | 105 ++++++++++++++-- src/Avalonia.Controls/RowDefinition.cs | 6 +- .../Utils/SharedSizeScopeHost.cs | 4 +- 5 files changed, 164 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index d316881a05..8c9f6323a9 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ColumnDefinition() + public ColumnDefinition() : base(true) { } @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// The width of the column. /// The width unit of the column. - public ColumnDefinition(double value, GridUnitType type) + public ColumnDefinition(double value, GridUnitType type): base(true) { Width = new GridLength(value, type); } @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The width of the column. - public ColumnDefinition(GridLength width) + public ColumnDefinition(GridLength width): base(true) { Width = width; } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index e1c5979476..b25ae3ab59 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.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; using System.Collections.Generic; using System.Diagnostics; using Avalonia.Utilities; @@ -14,6 +15,13 @@ namespace Avalonia.Controls /// public class DefinitionBase : ContentControl { + /// + /// Static ctor. Used for static registration of properties. + /// + static DefinitionBase() + { + SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + } /// /// Defines the property. @@ -57,21 +65,21 @@ namespace Avalonia.Controls /// internal void OnEnterParentTree() { - 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); - } - } - } + // 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); + // } + // } + // } } /// @@ -124,7 +132,7 @@ namespace Avalonia.Controls /// /// This method needs to be internal to be accessable from derived classes. /// - internal static void OnUserSizePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + internal static void OnUserSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { DefinitionBase definition = (DefinitionBase)d; @@ -167,7 +175,7 @@ namespace Avalonia.Controls /// /// This method needs to be internal to be accessable from derived classes. /// - internal static void OnUserMinSizePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + internal static void OnUserMinSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { DefinitionBase definition = (DefinitionBase)d; @@ -187,7 +195,7 @@ namespace Avalonia.Controls internal static bool IsUserMinSizePropertyValueValid(object value) { double v = (double)value; - return (!MathUtilities.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); + return (!Double.IsNaN(v) && v >= 0.0d && !Double.IsPositiveInfinity(v)); } /// @@ -196,7 +204,7 @@ namespace Avalonia.Controls /// /// This method needs to be internal to be accessable from derived classes. /// - internal static void OnUserMaxSizePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + internal static void OnUserMaxSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { DefinitionBase definition = (DefinitionBase)d; @@ -216,7 +224,7 @@ namespace Avalonia.Controls internal static bool IsUserMaxSizePropertyValueValid(object value) { double v = (double)value; - return (!MathUtilities.IsNaN(v) && v >= 0.0d); + return (!Double.IsNaN(v) && v >= 0.0d); } /// @@ -230,7 +238,7 @@ namespace Avalonia.Controls /// 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(DependencyObject d, DependencyPropertyChangedEventArgs e) + 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) @@ -238,11 +246,11 @@ namespace Avalonia.Controls if ((bool)e.NewValue) { SharedSizeScope sharedStatesCollection = new SharedSizeScope(); - d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); + // d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection); } else { - d.ClearValue(PrivateSharedSizeScopeProperty); + // d.ClearValue(PrivateSharedSizeScopeProperty); } } @@ -307,7 +315,7 @@ namespace Avalonia.Controls /// /// Layout-time user size type. /// - internal Grid.GridLayoutTimeSizeType SizeType + internal Grid.LayoutTimeSizeType SizeType { get { return (_sizeType); } set { _sizeType = value; } @@ -333,7 +341,7 @@ namespace Avalonia.Controls get { double preferredSize = MinSize; - if (_sizeType != Grid.GridLayoutTimeSizeType.Auto + if (_sizeType != Grid.LayoutTimeSizeType.Auto && preferredSize < _measureSize) { preferredSize = _measureSize; @@ -477,10 +485,8 @@ namespace Avalonia.Controls /// /// /// - private static void OnSharedSizeGroupPropertyChanged(AvaloniaPropertyChangedEventArgs e) + private static void OnSharedSizeGroupPropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) { - DefinitionBase definition = (DefinitionBase)e.Sender; - if (definition.InParentLogicalTree) { string sharedSizeGroupId = (string)e.NewValue; @@ -495,14 +501,14 @@ namespace Avalonia.Controls 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); - } + // 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); + // } } } } @@ -513,12 +519,12 @@ namespace Avalonia.Controls /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// - private static bool SharedSizeGroupPropertyValueValid(string value) + private static string SharedSizeGroupPropertyValueValid(DefinitionBase _, string value) { // null is default value if (value == null) { - return (true); + return value; } string id = (string)value; @@ -541,11 +547,11 @@ namespace Avalonia.Controls if (i == id.Length) { - return (true); + return value; } } - return (false); + return null; } /// @@ -596,13 +602,13 @@ namespace Avalonia.Controls #region Private Properties - /// - /// Private getter of shared state collection dynamic property. - /// - private SharedSizeScope PrivateSharedSizeScope - { - get { return (SharedSizeScope)GetValue(PrivateSharedSizeScopeProperty); } - } + // /// + // /// Private getter of shared state collection dynamic property. + // /// + // private SharedSizeScope PrivateSharedSizeScope + // { + // get { return (SharedSizeScope)GetValue(PrivateSharedSizeScopeProperty); } + // } /// /// Convenience accessor to UseSharedMinimum flag @@ -635,7 +641,7 @@ namespace Avalonia.Controls private Flags _flags; // flags reflecting various aspects of internal state private int _parentIndex; // this instance's index in parent's children collection - private Grid.GridLayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content" + 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 @@ -716,7 +722,7 @@ namespace Avalonia.Controls _sharedSizeScope = sharedSizeScope; _sharedSizeGroupId = sharedSizeGroupId; _registry = new List(); - _layoutUpdated = new EventHandler(OnLayoutUpdated); + // _layoutUpdated = new EventHandler(OnLayoutUpdated); _broadcastInvalidation = true; } @@ -775,7 +781,8 @@ namespace Avalonia.Controls if (_layoutUpdatedHost == null) { _layoutUpdatedHost = layoutUpdatedHost; - _layoutUpdatedHost.LayoutUpdated += _layoutUpdated; + // PORTING HACK... Remove this when resolved. + _layoutUpdatedHost.GetSubject(Visual.BoundsProperty).Subscribe(p => _layoutUpdated?.Invoke(this, null)); } } @@ -884,7 +891,7 @@ namespace Avalonia.Controls _minSize = sharedMinSize; - _layoutUpdatedHost.LayoutUpdated -= _layoutUpdated; + // _layoutUpdatedHost.LayoutUpdated -= _layoutUpdated; _layoutUpdatedHost = null; _broadcastInvalidation = true; @@ -902,14 +909,6 @@ namespace Avalonia.Controls } - /// - /// Static ctor. Used for static registration of properties. - /// - static DefinitionBase() - { - SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); - } - #endregion Properties } } diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index d42d6fc651..c9397e2e20 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -84,6 +84,98 @@ namespace Avalonia.Controls set { SetValue(ShowGridLinesProperty, value); } } + + /// + /// Gets the value of the Column attached property for a control. + /// + /// The control. + /// The control's column. + public static int GetColumn(AvaloniaObject element) + { + return element.GetValue(ColumnProperty); + } + + /// + /// Gets the value of the ColumnSpan attached property for a control. + /// + /// The control. + /// The control's column span. + public static int GetColumnSpan(AvaloniaObject element) + { + return element.GetValue(ColumnSpanProperty); + } + + /// + /// Gets the value of the Row attached property for a control. + /// + /// The control. + /// The control's row. + public static int GetRow(AvaloniaObject element) + { + return element.GetValue(RowProperty); + } + + /// + /// Gets the value of the RowSpan attached property for a control. + /// + /// The control. + /// The control's row span. + public static int GetRowSpan(AvaloniaObject element) + { + return element.GetValue(RowSpanProperty); + } + + + /// + /// Gets the value of the IsSharedSizeScope attached property for a control. + /// + /// The control. + /// The control's IsSharedSizeScope value. + public static bool GetIsSharedSizeScope(AvaloniaObject element) + { + return element.GetValue(IsSharedSizeScopeProperty); + } + + /// + /// Sets the value of the Column attached property for a control. + /// + /// The control. + /// The column value. + public static void SetColumn(AvaloniaObject element, int value) + { + element.SetValue(ColumnProperty, value); + } + + /// + /// Sets the value of the ColumnSpan attached property for a control. + /// + /// The control. + /// The column span value. + public static void SetColumnSpan(AvaloniaObject element, int value) + { + element.SetValue(ColumnSpanProperty, value); + } + + /// + /// Sets the value of the Row attached property for a control. + /// + /// The control. + /// The row value. + public static void SetRow(AvaloniaObject element, int value) + { + element.SetValue(RowProperty, value); + } + + /// + /// Sets the value of the RowSpan attached property for a control. + /// + /// The control. + /// The row span value. + public static void SetRowSpan(AvaloniaObject element, int value) + { + element.SetValue(RowSpanProperty, value); + } + private ColumnDefinitions _columnDefinitions; private RowDefinitions _rowDefinitions; @@ -484,12 +576,11 @@ namespace Avalonia.Controls for (int i = PrivateCells.Length - 1; i >= 0; --i) { - var child = Children[i]; + var child = Children[i] as Control; if (child == null) { continue; } - CellCache cell = new CellCache(); // @@ -499,20 +590,20 @@ namespace Avalonia.Controls // 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(child.GetValue(Grid.ColumnProperty), DefinitionsU.Length - 1); + cell.ColumnIndex = Math.Min(GetColumn(child), DefinitionsU.Length - 1); // clamp to value < number_of_rows // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(child.GetValue(Grid.RowProperty), DefinitionsV.Length - 1); + cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Length - 1); // read span properties // clamp to not exceed beyond right side of the grid // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(child.GetValue(Grid.ColumnSpanProperty), DefinitionsU.Length - cell.ColumnIndex); + cell.ColumnSpan = Math.Min(GetColumnSpan(child), DefinitionsU.Length - cell.ColumnIndex); // clamp to not exceed beyond bottom side of the grid // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(child.GetValue(Grid.RowSpanProperty), DefinitionsV.Length - cell.RowIndex); - + cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Length - cell.RowIndex); + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Length); Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Length); diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 7307843417..d42ffdfc28 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() : base(false) { } @@ -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): base(false) { 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): base(false) { Height = height; } diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index 8553165e4b..5f70557385 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -395,8 +395,8 @@ namespace Avalonia.Controls /// public void Dispose() { - while (_measurementCaches.Any()) - _measurementCaches[0].Grid.SharedScopeChanged(); + // while (_measurementCaches.Any()) + // _measurementCaches[0].Grid.SharedScopeChanged(); } /// From efdcd1195222caf1e60ee4f068df142ca0cf463a Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 24 May 2019 23:22:51 +0800 Subject: [PATCH 037/179] Part 9 of n --- src/Avalonia.Controls/DefinitionBase.cs | 1 + src/Avalonia.Controls/GridWPF.cs | 27 +++++++++++-------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index b25ae3ab59..f9a7fc648d 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -21,6 +21,7 @@ namespace Avalonia.Controls static DefinitionBase() { SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + BoundsProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); } /// diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index c9397e2e20..e85fa1cdb0 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -80,11 +80,10 @@ namespace Avalonia.Controls /// public bool ShowGridLines { - get { return (CheckFlagsAnd(Flags.ShowGridLinesPropertyValue)); } + get { return GetValue(ShowGridLinesProperty); } set { SetValue(ShowGridLinesProperty, value); } } - /// /// Gets the value of the Column attached property for a control. /// @@ -186,7 +185,6 @@ namespace Avalonia.Controls { get { - if (_data == null) { _data = new ExtendedData(); } if (_columnDefinitions == null) { @@ -204,6 +202,9 @@ namespace Avalonia.Controls throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); } + if (_data == null) { _data = new ExtendedData(); } + + _columnDefinitions = value; _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); @@ -217,8 +218,6 @@ namespace Avalonia.Controls { get { - if (_data == null) { _data = new ExtendedData(); } - if (_rowDefinitions == null) { RowDefinitions = new RowDefinitions(); @@ -234,12 +233,17 @@ namespace Avalonia.Controls throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); } + if (_data == null) { _data = new ExtendedData(); } + _rowDefinitions = value; _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); } } + private bool rowColDefsEmpty => (RowDefinitions == null || RowDefinitions?.Count == 0 ) && + (ColumnDefinitions == null || ColumnDefinitions?.Count == 0); + /// /// Content measurement. /// @@ -255,7 +259,7 @@ namespace Avalonia.Controls ListenToNotifications = true; MeasureOverrideInProgress = true; - if (extData == null) + if (rowColDefsEmpty) { gridDesiredSize = new Size(); @@ -400,7 +404,7 @@ namespace Avalonia.Controls ArrangeOverrideInProgress = true; - if (_data == null) + if (rowColDefsEmpty) { for (int i = 0, count = Children.Count; i < count; ++i) { @@ -603,7 +607,7 @@ namespace Avalonia.Controls // clamp to not exceed beyond bottom side of the grid // row_span > 0 is guaranteed by property value validation callback cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Length - cell.RowIndex); - + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Length); Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Length); @@ -2275,8 +2279,6 @@ namespace Avalonia.Controls { grid.InvalidateVisual(); } - - grid.SetFlags((bool)e.NewValue, Flags.ShowGridLinesPropertyValue); } private static void OnCellAttachedPropertyChanged(Visual child, AvaloniaPropertyChangedEventArgs e) @@ -2590,11 +2592,6 @@ namespace Avalonia.Controls ValidDefinitionsVStructure = 0x00000002, ValidCellsStructure = 0x00000004, - // - // boolean properties state - // - ShowGridLinesPropertyValue = 0x00000100, // show grid lines ? - // // boolean flags // From 9dc40dc99bb4555d0b76614103ea5a7baca2e43c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sat, 25 May 2019 11:39:40 +0800 Subject: [PATCH 038/179] Temporarily hook row and col def change to ExtData. --- src/Avalonia.Controls/GridWPF.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index e85fa1cdb0..89ae5c6bf5 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -207,7 +207,12 @@ namespace Avalonia.Controls _columnDefinitions = value; _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + _columnDefinitions.CollectionChanged += (_, __) => + { + _data.DefinitionsU = _columnDefinitions.Select(p => (DefinitionBase)p).ToArray(); + InvalidateMeasure(); + }; + } } @@ -237,11 +242,15 @@ namespace Avalonia.Controls _rowDefinitions = value; _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); + _rowDefinitions.CollectionChanged += (_, __) => + { + _data.DefinitionsV = _rowDefinitions.Select(p => (DefinitionBase)p).ToArray(); + InvalidateMeasure(); + }; } } - private bool rowColDefsEmpty => (RowDefinitions == null || RowDefinitions?.Count == 0 ) && + private bool rowColDefsEmpty => (RowDefinitions == null || RowDefinitions?.Count == 0) && (ColumnDefinitions == null || ColumnDefinitions?.Count == 0); /// From 676688b3de9fed0d10b9deb684fa9aa1f0422891 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 25 May 2019 22:05:11 +0300 Subject: [PATCH 039/179] Set PackageId for Avalonia.csproj Hopefully that will fix UsedBy thingy --- packages/Avalonia/Avalonia.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 489cb228aa..2c5a09bee7 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -1,6 +1,7 @@  netstandard2.0;net461;netcoreapp2.0 + Avalonia From dbc284d96e0aec72d60a1a87cad0e149bc5ef9f5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 25 May 2019 22:24:52 +0300 Subject: [PATCH 040/179] Set PackageId for github's dependency graph to work --- .../Avalonia.Controls.DataGrid.csproj | 1 + src/Avalonia.Desktop/Avalonia.Desktop.csproj | 1 + src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj | 1 + src/Avalonia.X11/Avalonia.X11.csproj | 2 +- src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj | 1 + .../Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj | 1 + src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 1 + src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj | 1 + src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 1 + 9 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj index 21d34ae4d6..27853a1540 100644 --- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj +++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia.Controls.DataGrid diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj index f7c06306f5..f45b0c54e3 100644 --- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj +++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj @@ -1,6 +1,7 @@ netstandard2.0 + Avalonia.Desktop diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj index 1851f9fb70..64145b9c3c 100644 --- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj +++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia.ReactiveUI diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 1629890568..59afc877de 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -1,8 +1,8 @@  - netstandard2.0 true + Avalonia.X11 diff --git a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj index 0885ccccfa..95443a364e 100644 --- a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj +++ b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj @@ -3,6 +3,7 @@ netstandard2.0 true $(DefineConstants);GTK3_PINVOKE + Avalonia.Gtk3 diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj index c622bbb8c5..48095a4c25 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -2,6 +2,7 @@ netstandard2.0 true + Avalonia.LinuxFramebuffer diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 0479a74937..4f884cdf33 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -3,6 +3,7 @@ netstandard2.0 Avalonia.Skia Avalonia.Skia + Avalonia.Skia true true diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 70b2703f5e..16f2767096 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -1,6 +1,7 @@  netstandard2.0 + Avalonia.Direct2D1 diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index d1046c6133..67db56dd1b 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -2,6 +2,7 @@ netstandard2.0 true + Avalonia.Win32 From 9b1eb2ac8c752b9b3061a1d60cee2d0e7adb6a20 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 00:29:43 +0800 Subject: [PATCH 041/179] Part 10 of n --- src/Avalonia.Controls/ColumnDefinition.cs | 12 +- src/Avalonia.Controls/DefinitionBase.cs | 370 ++-------------------- src/Avalonia.Controls/GridWPF.cs | 209 +++--------- src/Avalonia.Controls/RowDefinition.cs | 13 +- 4 files changed, 101 insertions(+), 503 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index 8c9f6323a9..015484dbcc 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - public ColumnDefinition() : base(true) + public ColumnDefinition() { } @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// The width of the column. /// The width unit of the column. - public ColumnDefinition(double value, GridUnitType type): base(true) + public ColumnDefinition(double value, GridUnitType type) { Width = new GridLength(value, type); } @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// Initializes a new instance of the class. /// /// The width of the column. - public ColumnDefinition(GridLength width): base(true) + public ColumnDefinition(GridLength width) { Width = width; } @@ -87,5 +87,11 @@ 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/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index f9a7fc648d..643d9da751 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// /// Base class for and . /// - public class DefinitionBase : ContentControl + public abstract class DefinitionBase : ContentControl { /// /// Static ctor. Used for static registration of properties. @@ -21,8 +21,9 @@ namespace Avalonia.Controls static DefinitionBase() { SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); - BoundsProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); + // BoundsProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); } + /// /// Defines the property. @@ -38,63 +39,6 @@ namespace Avalonia.Controls get { return GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } - //------------------------------------------------------ - // - // Constructors - // - //------------------------------------------------------ - - - internal DefinitionBase(bool isColumnDefinition) - { - _isColumnDefinition = isColumnDefinition; - _parentIndex = -1; - } - - - - //------------------------------------------------------ - // - // Internal Methods - // - //------------------------------------------------------ - - #region Internal Methods - - /// - /// Callback to notify about entering model tree. - /// - internal void OnEnterParentTree() - { - // 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); - // } - // } - // } - } - - /// - /// 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. @@ -109,161 +53,6 @@ namespace Avalonia.Controls 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 static void OnUserSizePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - DefinitionBase definition = (DefinitionBase)d; - - if (definition.InParentLogicalTree) - { - if (definition._sharedState != null) - { - definition._sharedState.Invalidate(); - } - else - { - Grid parentGrid = (Grid)definition.Parent; - - if (((GridLength)e.OldValue).GridUnitType != ((GridLength)e.NewValue).GridUnitType) - { - parentGrid.Invalidate(); - } - else - { - parentGrid.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. @@ -376,6 +165,25 @@ namespace Avalonia.Controls } return (minSize); } + + } + + /// + /// 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; } /// @@ -408,44 +216,18 @@ namespace Avalonia.Controls /// /// Internal helper to access up-to-date UserSize property value. /// - internal GridLength UserSizeValueCache - { - get - { - return (GridLength)GetValue( - _isColumnDefinition ? - ColumnDefinition.WidthProperty : - RowDefinition.HeightProperty); - } - } + internal abstract GridLength UserSizeValueCache { get; } /// /// Internal helper to access up-to-date UserMinSize property value. /// - internal double UserMinSizeValueCache - { - get - { - return (double)GetValue( - _isColumnDefinition ? - ColumnDefinition.MinWidthProperty : - RowDefinition.MinHeightProperty); - } - } + internal abstract double UserMinSizeValueCache { get; } /// /// Internal helper to access up-to-date UserMaxSize property value. /// - internal double UserMaxSizeValueCache - { - get - { - return (double)GetValue( - _isColumnDefinition ? - ColumnDefinition.MaxWidthProperty : - RowDefinition.MaxHeightProperty); - } - } + internal abstract double UserMaxSizeValueCache { get; } + /// /// Protected. Returns true if this DefinitionBase instance is in parent's logical tree. @@ -455,37 +237,6 @@ namespace Avalonia.Controls get { return (_parentIndex != -1); } } - #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(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) { if (definition.InParentLogicalTree) @@ -593,31 +344,13 @@ namespace Avalonia.Controls } } - #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); } + get { return _useSharedMinimum; } + set { _useSharedMinimum = value; } } /// @@ -625,22 +358,11 @@ namespace Avalonia.Controls /// private bool LayoutWasUpdated { - get { return (CheckFlagsAnd(Flags.LayoutWasUpdated)); } - set { SetFlags(value, Flags.LayoutWasUpdated); } + get { return _layoutWasUpdated; } + set { _layoutWasUpdated = value; } } - #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 - private int _parentIndex; // this instance's index in parent's children collection + private 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" @@ -650,29 +372,10 @@ namespace Avalonia.Controls 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 + private bool _layoutWasUpdated; + private bool _useSharedMinimum; - 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 @@ -908,10 +611,5 @@ namespace Avalonia.Controls private GridLength _userSize; // shared state private double _minSize; // shared state } - - - #endregion Properties } -} - - +} \ No newline at end of file diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 89ae5c6bf5..d049f9fd4b 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -186,6 +186,7 @@ namespace Avalonia.Controls get { + if (_columnDefinitions == null) { ColumnDefinitions = new ColumnDefinitions(); @@ -207,9 +208,13 @@ namespace Avalonia.Controls _columnDefinitions = value; _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => + ColumnDefinitionsDirty = true; + DefinitionsU = _columnDefinitions.Cast().ToArray(); + + _columnDefinitions.CollectionChanged += (_, e) => { - _data.DefinitionsU = _columnDefinitions.Select(p => (DefinitionBase)p).ToArray(); + DefinitionsU = e.NewItems.Cast().ToArray(); + ColumnDefinitionsDirty = true; InvalidateMeasure(); }; @@ -242,16 +247,21 @@ namespace Avalonia.Controls _rowDefinitions = value; _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => + RowDefinitionsDirty = true; + + DefinitionsV = _rowDefinitions.Cast().ToArray(); + + _rowDefinitions.CollectionChanged += (_, e) => { - _data.DefinitionsV = _rowDefinitions.Select(p => (DefinitionBase)p).ToArray(); + DefinitionsV = e.NewItems.Cast().ToArray(); + RowDefinitionsDirty = true; InvalidateMeasure(); }; } } - private bool rowColDefsEmpty => (RowDefinitions == null || RowDefinitions?.Count == 0) && - (ColumnDefinitions == null || ColumnDefinitions?.Count == 0); + private bool rowColDefsEmpty => (DefinitionsU.Length == 0) && + (DefinitionsV.Length == 0); /// /// Content measurement. @@ -309,10 +319,10 @@ namespace Avalonia.Controls } } - ValidateDefinitionsUStructure(); + // ValidateColumnDefinitionsStructure(); ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); - ValidateDefinitionsVStructure(); + // ValidateRowDefinitionsStructure(); ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); @@ -453,12 +463,12 @@ namespace Avalonia.Controls cell.Arrange(cellRect); } - // update render bound on grid lines renderer visual - var gridLinesRenderer = EnsureGridLinesRenderer(); - if (gridLinesRenderer != null) - { - gridLinesRenderer.UpdateRenderBounds(arrangeSize); - } + // // update render bound on grid lines renderer visual + // var gridLinesRenderer = EnsureGridLinesRenderer(); + // if (gridLinesRenderer != null) + // { + // gridLinesRenderer.UpdateRenderBounds(arrangeSize); + // } } } finally @@ -493,7 +503,7 @@ namespace Avalonia.Controls // actual value calculations require structure to be up-to-date if (!ColumnDefinitionsDirty) { - DefinitionBase[] definitions = DefinitionsU; + var definitions = DefinitionsU; value = definitions[(columnIndex + 1) % definitions.Length].FinalOffset; if (columnIndex != 0) { value -= definitions[columnIndex].FinalOffset; } } @@ -515,7 +525,7 @@ namespace Avalonia.Controls // actual value calculations require structure to be up-to-date if (!RowDefinitionsDirty) { - DefinitionBase[] definitions = DefinitionsV; + var definitions = DefinitionsV; value = definitions[(rowIndex + 1) % definitions.Length].FinalOffset; if (rowIndex != 0) { value -= definitions[rowIndex].FinalOffset; } } @@ -525,38 +535,20 @@ namespace Avalonia.Controls /// /// Convenience accessor to MeasureOverrideInProgress bit flag. /// - internal bool MeasureOverrideInProgress - { - get { return (CheckFlagsAnd(Flags.MeasureOverrideInProgress)); } - set { SetFlags(value, Flags.MeasureOverrideInProgress); } - } - + internal bool MeasureOverrideInProgress { get; set; } /// /// Convenience accessor to ArrangeOverrideInProgress bit flag. /// - internal bool ArrangeOverrideInProgress - { - get { return (CheckFlagsAnd(Flags.ArrangeOverrideInProgress)); } - set { SetFlags(value, Flags.ArrangeOverrideInProgress); } - } - + internal bool ArrangeOverrideInProgress { get; set; } /// - /// Convenience accessor to ValidDefinitionsUStructure bit flag. + /// Convenience accessor to ValidColumnDefinitionsStructure bit flag. /// - internal bool ColumnDefinitionsDirty - { - get { return (!CheckFlagsAnd(Flags.ValidDefinitionsUStructure)); } - set { SetFlags(!value, Flags.ValidDefinitionsUStructure); } - } + internal bool ColumnDefinitionsDirty { get; set; } /// - /// Convenience accessor to ValidDefinitionsVStructure bit flag. + /// Convenience accessor to ValidRowDefinitionsStructure bit flag. /// - internal bool RowDefinitionsDirty - { - get { return (!CheckFlagsAnd(Flags.ValidDefinitionsVStructure)); } - set { SetFlags(!value, Flags.ValidDefinitionsVStructure); } - } + internal bool RowDefinitionsDirty { get; set; } /// /// Lays out cells according to rows and columns, and creates lookup grids. @@ -674,92 +666,6 @@ namespace Avalonia.Controls 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() }; - } - } - else - { - // extData.ColumnDefinitions.InternalTrimToSize(); - - if (extData.ColumnDefinitions.Count == 0) - { - // if column definitions collection is empty - // mockup array with one column - extData.DefinitionsU = new DefinitionBase[] { new ColumnDefinition() }; - } - else - { - extData.DefinitionsU = extData.ColumnDefinitions.ToArray(); - } - } - - ColumnDefinitionsDirty = false; - } - - Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Length > 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() }; - } - } - else - { - // extData.RowDefinitions.InternalTrimToSize(); - - if (extData.RowDefinitions.Count == 0) - { - // if row definitions collection is empty - // mockup array with one row - extData.DefinitionsV = new DefinitionBase[] { new RowDefinition() }; - } - else - { - extData.DefinitionsV = extData.RowDefinitions.ToArray(); - } - } - - RowDefinitionsDirty = false; - } - - Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Length > 0); - } - /// /// Validates layout time size type information on given array of definitions. /// Sets MinSize and MeasureSizes. @@ -1607,7 +1513,7 @@ namespace Avalonia.Controls /// Array of definitions to use for calculations. /// Desired size. private double CalculateDesiredSize( - DefinitionBase[] definitions) + DefinitionBase[] definitions) { double desiredSize = 0; @@ -1762,9 +1668,9 @@ namespace Avalonia.Controls double remainingAvailableSize = finalSize - takenSize; double remainingStarWeight = totalStarWeight - takenStarWeight; - MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); + MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer((DefinitionBase[])definitions); Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); - MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); + MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer((DefinitionBase[])definitions); Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) @@ -2147,7 +2053,7 @@ namespace Avalonia.Controls /// Number of items in the range. /// Final size. private double GetFinalSizeForRange( - DefinitionBase[] definitions, + DefinitionBase[] definitions, int start, int count) { @@ -2338,22 +2244,6 @@ namespace Avalonia.Controls return (result != 2); } - /// - /// Private version returning array of column definitions. - /// - private DefinitionBase[] DefinitionsU - { - get { return (ExtData.DefinitionsU); } - } - - /// - /// Private version returning array of row definitions. - /// - private DefinitionBase[] DefinitionsV - { - get { return (ExtData.DefinitionsV); } - } - /// /// Helper accessor to layout time array of definitions. /// @@ -2545,7 +2435,7 @@ namespace Avalonia.Controls } } - private ExtendedData _data; // extended data instantiated on demand, for non-trivial case handling only + private ExtendedData _data = new ExtendedData(); // extended data instantiated on demand, for non-trivial case handling only private Flags _flags; // grid validity / property caches dirtiness flags private GridLinesRenderer _gridLinesRenderer; @@ -2554,7 +2444,8 @@ namespace Avalonia.Controls // Stores unrounded values and rounding errors during layout rounding. double[] _roundingErrors; - + private DefinitionBase[] DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + private DefinitionBase[] DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; 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 @@ -2572,17 +2463,13 @@ namespace Avalonia.Controls /// private class ExtendedData { - internal ColumnDefinitions ColumnDefinitions; // collection of column definitions (logical tree support) - internal RowDefinitions RowDefinitions; // collection of row definitions (logical tree support) - internal DefinitionBase[] DefinitionsU; // collection of column definitions used during calc - internal DefinitionBase[] DefinitionsV; // collection of row definitions used during calc internal CellCache[] CellCachesCollection; // backing store for logical Children internal int CellGroup1; // index of the first cell in first cell group internal int CellGroup2; // index of the first cell in second cell group internal int CellGroup3; // index of the first cell in third cell group internal int CellGroup4; // index of the first cell in forth cell group internal DefinitionBase[] TempDefinitions; // temporary array used during layout for various purposes - // TempDefinitions.Length == Max(definitionsU.Length, definitionsV.Length) + // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) } /// @@ -2597,8 +2484,8 @@ namespace Avalonia.Controls // * Valid???Layout flags indicate that layout time portion of the information // stored on the objects should be updated. // - ValidDefinitionsUStructure = 0x00000001, - ValidDefinitionsVStructure = 0x00000002, + ValidColumnDefinitionsStructure = 0x00000001, + ValidRowDefinitionsStructure = 0x00000002, ValidCellsStructure = 0x00000004, // @@ -3188,20 +3075,20 @@ namespace Avalonia.Controls return; } - for (int i = 1; i < grid.DefinitionsU.Length; ++i) + for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) { DrawGridLine( drawingContext, - grid.DefinitionsU[i].FinalOffset, 0.0, - grid.DefinitionsU[i].FinalOffset, lastArrangeSize.Height); + grid.ColumnDefinitions[i].FinalOffset, 0.0, + grid.ColumnDefinitions[i].FinalOffset, lastArrangeSize.Height); } - for (int i = 1; i < grid.DefinitionsV.Length; ++i) + for (int i = 1; i < grid.RowDefinitions.Count; ++i) { DrawGridLine( drawingContext, - 0.0, grid.DefinitionsV[i].FinalOffset, - lastArrangeSize.Width, grid.DefinitionsV[i].FinalOffset); + 0.0, grid.RowDefinitions[i].FinalOffset, + lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); } } diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index d42ffdfc28..1cb09e16e9 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() : base(false) + 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): base(false) + 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): base(false) + public RowDefinition(GridLength height) { Height = height; } @@ -87,5 +87,12 @@ 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 From fa95d9cd10f7d0778a0a557c80caafa8c920334d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 03:26:17 +0800 Subject: [PATCH 042/179] Fix rowcol definitions not sync'ing with DefinitionsU/V, hence fixing bugs for the Calendar control. --- src/Avalonia.Controls/GridWPF.cs | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index d049f9fd4b..0cd61d0bb1 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -185,8 +185,6 @@ namespace Avalonia.Controls { get { - - if (_columnDefinitions == null) { ColumnDefinitions = new ColumnDefinitions(); @@ -194,18 +192,8 @@ namespace Avalonia.Controls return _columnDefinitions; } - set - { - - if (_columnDefinitions != null) - { - throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); - } - - if (_data == null) { _data = new ExtendedData(); } - - + { _columnDefinitions = value; _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); ColumnDefinitionsDirty = true; @@ -213,7 +201,7 @@ namespace Avalonia.Controls _columnDefinitions.CollectionChanged += (_, e) => { - DefinitionsU = e.NewItems.Cast().ToArray(); + DefinitionsU = _columnDefinitions.Cast().ToArray(); ColumnDefinitionsDirty = true; InvalidateMeasure(); }; @@ -235,25 +223,18 @@ namespace Avalonia.Controls return _rowDefinitions; } - set { - if (_rowDefinitions != null) - { - throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); - } - - if (_data == null) { _data = new ExtendedData(); } - _rowDefinitions = value; _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + RowDefinitionsDirty = true; DefinitionsV = _rowDefinitions.Cast().ToArray(); _rowDefinitions.CollectionChanged += (_, e) => { - DefinitionsV = e.NewItems.Cast().ToArray(); + DefinitionsV = _rowDefinitions.Cast().ToArray(); RowDefinitionsDirty = true; InvalidateMeasure(); }; From 0e15b3f972d132ec430a30d46ab76a9d4909c874 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 04:10:26 +0800 Subject: [PATCH 043/179] Cleaning up redundant/unreferenced code. --- src/Avalonia.Controls/GridWPF.cs | 653 ++++++++----------------------- 1 file changed, 157 insertions(+), 496 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 0cd61d0bb1..0549c7f20a 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -26,6 +26,49 @@ namespace Avalonia.Controls /// public class Grid : Panel { + internal bool CellsStructureDirty = true; + internal bool ListenToNotifications; + internal bool SizeToContentU; + internal bool SizeToContentV; + internal bool HasStarCellsU; + internal bool HasStarCellsV; + internal bool HasGroup3CellsInAutoRows; + + // index of the first cell in first cell group + internal int CellGroup1; + + // index of the first cell in second cell group + internal int CellGroup2; + + // index of the first cell in third cell group + internal int CellGroup3; + + // index of the first cell in fourth cell group + + internal int CellGroup4; + + // temporary array used during layout for various purposes + // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) + internal DefinitionBase[] _tempDefinitions; + private GridLinesRenderer _gridLinesRenderer; + + // Keeps track of definition indices. + private int[] _definitionIndices; + + private CellCache[] _cellCache; + + + // Stores unrounded values and rounding errors during layout rounding. + private double[] _roundingErrors; + private DefinitionBase[] DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + private DefinitionBase[] DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + 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_minRatioComparer = new MinRatioComparer(); + private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); + private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); static Grid() { @@ -193,19 +236,27 @@ namespace Avalonia.Controls return _columnDefinitions; } set - { + { _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); ColumnDefinitionsDirty = true; - DefinitionsU = _columnDefinitions.Cast().ToArray(); + + if (_columnDefinitions.Count > 0) + DefinitionsU = _columnDefinitions.Cast().ToArray(); _columnDefinitions.CollectionChanged += (_, e) => { - DefinitionsU = _columnDefinitions.Cast().ToArray(); - ColumnDefinitionsDirty = true; - InvalidateMeasure(); + if (_columnDefinitions.Count == 0) + { + DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + } + else + { + DefinitionsU = _columnDefinitions.Cast().ToArray(); + ColumnDefinitionsDirty = true; + } + Invalidate(); }; - } } @@ -226,17 +277,25 @@ namespace Avalonia.Controls set { _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); + _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); RowDefinitionsDirty = true; - DefinitionsV = _rowDefinitions.Cast().ToArray(); + if (_rowDefinitions.Count > 0) + DefinitionsV = _rowDefinitions.Cast().ToArray(); _rowDefinitions.CollectionChanged += (_, e) => { - DefinitionsV = _rowDefinitions.Cast().ToArray(); - RowDefinitionsDirty = true; - InvalidateMeasure(); + if (_rowDefinitions.Count == 0) + { + DefinitionsV = new DefinitionBase[1] { new ColumnDefinition() }; + } + else + { + DefinitionsV = _rowDefinitions.Cast().ToArray(); + RowDefinitionsDirty = true; + } + Invalidate(); }; } } @@ -252,7 +311,6 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size constraint) { Size gridDesiredSize; - ExtendedData extData = ExtData; try { @@ -316,7 +374,7 @@ namespace Avalonia.Controls Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); - MeasureCellsGroup(extData.CellGroup1, constraint, false, false); + MeasureCellsGroup(CellGroup1, constraint, false, false); { // after Group1 is measured, only Group3 may have cells belonging to Auto rows. @@ -325,19 +383,19 @@ namespace Avalonia.Controls if (canResolveStarsV) { if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(extData.CellGroup2, constraint, false, false); + MeasureCellsGroup(CellGroup2, constraint, false, false); if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + MeasureCellsGroup(CellGroup3, constraint, false, false); } else { // if at least one cell exists in Group2, it must be measured before // StarsU can be resolved. - bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length; + bool canResolveStarsU = CellGroup2 > _cellCache.Length; if (canResolveStarsU) { if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + MeasureCellsGroup(CellGroup3, constraint, false, false); if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } } else @@ -351,10 +409,10 @@ namespace Avalonia.Controls int cnt = 0; // Cache Group2MinWidths & Group3MinHeights - double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); - double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true); + double[] group2MinSizes = CacheMinSizes(CellGroup2, false); + double[] group3MinSizes = CacheMinSizes(CellGroup3, true); - MeasureCellsGroup(extData.CellGroup2, constraint, false, true); + MeasureCellsGroup(CellGroup2, constraint, false, true); do { @@ -365,20 +423,20 @@ namespace Avalonia.Controls } if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + MeasureCellsGroup(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); + MeasureCellsGroup(CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); } while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); } } } - MeasureCellsGroup(extData.CellGroup4, constraint, false, false); + MeasureCellsGroup(CellGroup4, constraint, false, false); gridDesiredSize = new Size( CalculateDesiredSize(DefinitionsU), @@ -422,7 +480,7 @@ namespace Avalonia.Controls SetFinalSize(DefinitionsU, arrangeSize.Width, true); SetFinalSize(DefinitionsV, arrangeSize.Height, false); - for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) + for (int currentCell = 0; currentCell < _cellCache.Length; ++currentCell) { IControl cell = Children[currentCell]; if (cell == null) @@ -430,10 +488,10 @@ namespace Avalonia.Controls continue; } - int columnIndex = PrivateCells[currentCell].ColumnIndex; - int rowIndex = PrivateCells[currentCell].RowIndex; - int columnSpan = PrivateCells[currentCell].ColumnSpan; - int rowSpan = PrivateCells[currentCell].RowSpan; + int columnIndex = _cellCache[currentCell].ColumnIndex; + int rowIndex = _cellCache[currentCell].RowIndex; + int columnSpan = _cellCache[currentCell].ColumnSpan; + int rowSpan = _cellCache[currentCell].RowSpan; Rect cellRect = new Rect( columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, @@ -469,109 +527,38 @@ namespace Avalonia.Controls 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) - { - var definitions = DefinitionsU; - value = definitions[(columnIndex + 1) % definitions.Length].FinalOffset; - if (columnIndex != 0) { value -= definitions[columnIndex].FinalOffset; } - } - return (value); - } - - /// - /// Returns final height for a row. - /// - /// - /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. - /// - internal double GetFinalRowDefinitionHeight(int rowIndex) - { - double value = 0.0; - - Contract.Requires(_data != null); - - // actual value calculations require structure to be up-to-date - if (!RowDefinitionsDirty) - { - var definitions = DefinitionsV; - value = definitions[(rowIndex + 1) % definitions.Length].FinalOffset; - if (rowIndex != 0) { value -= definitions[rowIndex].FinalOffset; } - } - return (value); - } - - /// - /// Convenience accessor to MeasureOverrideInProgress bit flag. - /// - internal bool MeasureOverrideInProgress { get; set; } - /// - /// Convenience accessor to ArrangeOverrideInProgress bit flag. - /// - internal bool ArrangeOverrideInProgress { get; set; } - /// - /// Convenience accessor to ValidColumnDefinitionsStructure bit flag. - /// - internal bool ColumnDefinitionsDirty { get; set; } - - /// - /// Convenience accessor to ValidRowDefinitionsStructure bit flag. - /// - internal bool RowDefinitionsDirty { get; set; } + internal bool MeasureOverrideInProgress; + internal bool ArrangeOverrideInProgress; + internal bool ColumnDefinitionsDirty; + internal bool RowDefinitionsDirty; /// /// Lays out cells according to rows and columns, and creates lookup grids. /// private void ValidateCells() { - if (CellsStructureDirty) - { - ValidateCellsCore(); - CellsStructureDirty = false; - } - } + if (!CellsStructureDirty) return; - /// - /// ValidateCellsCore - /// - private void ValidateCellsCore() - { - 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; + _cellCache = new CellCache[Children.Count]; + CellGroup1 = int.MaxValue; + CellGroup2 = int.MaxValue; + CellGroup3 = int.MaxValue; + CellGroup4 = int.MaxValue; bool hasStarCellsU = false; bool hasStarCellsV = false; bool hasGroup3CellsInAutoRows = false; - for (int i = PrivateCells.Length - 1; i >= 0; --i) + for (int i = _cellCache.Length - 1; i >= 0; --i) { var child = Children[i] as Control; + if (child == null) { continue; } - CellCache cell = new CellCache(); - // - // read and cache child positioning properties - // + var cell = new CellCache(); // read indices from the corresponding properties // clamp to value < number_of_columns @@ -611,13 +598,13 @@ namespace Avalonia.Controls { if (!cell.IsStarU) { - cell.Next = extData.CellGroup1; - extData.CellGroup1 = i; + cell.Next = CellGroup1; + CellGroup1 = i; } else { - cell.Next = extData.CellGroup3; - extData.CellGroup3 = i; + cell.Next = CellGroup3; + CellGroup3 = i; // remember if this cell belongs to auto row hasGroup3CellsInAutoRows |= cell.IsAutoV; @@ -629,22 +616,24 @@ namespace Avalonia.Controls // note below: if spans through Star column it is NOT Auto && !cell.IsStarU) { - cell.Next = extData.CellGroup2; - extData.CellGroup2 = i; + cell.Next = CellGroup2; + CellGroup2 = i; } else { - cell.Next = extData.CellGroup4; - extData.CellGroup4 = i; + cell.Next = CellGroup4; + CellGroup4 = i; } } - PrivateCells[i] = cell; + _cellCache[i] = cell; } HasStarCellsU = hasStarCellsU; HasStarCellsV = hasStarCellsV; HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; + + CellsStructureDirty = false; } /// @@ -713,15 +702,15 @@ namespace Avalonia.Controls { if (isRows) { - minSizes[PrivateCells[i].RowIndex] = DefinitionsV[PrivateCells[i].RowIndex].MinSize; + minSizes[_cellCache[i].RowIndex] = DefinitionsV[_cellCache[i].RowIndex].MinSize; } else { - minSizes[PrivateCells[i].ColumnIndex] = DefinitionsU[PrivateCells[i].ColumnIndex].MinSize; + minSizes[_cellCache[i].ColumnIndex] = DefinitionsU[_cellCache[i].ColumnIndex].MinSize; } - i = PrivateCells[i].Next; - } while (i < PrivateCells.Length); + i = _cellCache[i].Next; + } while (i < _cellCache.Length); return minSizes; } @@ -773,7 +762,7 @@ namespace Avalonia.Controls { hasDesiredSizeUChanged = false; - if (cellsHead >= PrivateCells.Length) + if (cellsHead >= _cellCache.Length) { return; } @@ -792,16 +781,16 @@ namespace Avalonia.Controls if (!ignoreDesiredSizeU) { - if (PrivateCells[i].ColumnSpan == 1) + if (_cellCache[i].ColumnSpan == 1) { - DefinitionsU[PrivateCells[i].ColumnIndex].UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, DefinitionsU[PrivateCells[i].ColumnIndex].UserMaxSize)); + DefinitionsU[_cellCache[i].ColumnIndex].UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, DefinitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); } else { RegisterSpan( ref spanStore, - PrivateCells[i].ColumnIndex, - PrivateCells[i].ColumnSpan, + _cellCache[i].ColumnIndex, + _cellCache[i].ColumnSpan, true, Children[i].DesiredSize.Width); } @@ -809,23 +798,23 @@ namespace Avalonia.Controls if (!ignoreDesiredSizeV) { - if (PrivateCells[i].RowSpan == 1) + if (_cellCache[i].RowSpan == 1) { - DefinitionsV[PrivateCells[i].RowIndex].UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, DefinitionsV[PrivateCells[i].RowIndex].UserMaxSize)); + DefinitionsV[_cellCache[i].RowIndex].UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, DefinitionsV[_cellCache[i].RowIndex].UserMaxSize)); } else { RegisterSpan( ref spanStore, - PrivateCells[i].RowIndex, - PrivateCells[i].RowSpan, + _cellCache[i].RowIndex, + _cellCache[i].RowSpan, false, Children[i].DesiredSize.Height); } } - i = PrivateCells[i].Next; - } while (i < PrivateCells.Length); + i = _cellCache[i].Next; + } while (i < _cellCache.Length); if (spanStore != null) { @@ -887,8 +876,8 @@ namespace Avalonia.Controls double cellMeasureWidth; double cellMeasureHeight; - if (PrivateCells[cell].IsAutoU - && !PrivateCells[cell].IsStarU) + if (_cellCache[cell].IsAutoU + && !_cellCache[cell].IsStarU) { // if cell belongs to at least one Auto column and not a single Star column // then it should be calculated "to content", thus it is possible to "shortcut" @@ -900,16 +889,16 @@ namespace Avalonia.Controls // otherwise... cellMeasureWidth = GetMeasureSizeForRange( DefinitionsU, - PrivateCells[cell].ColumnIndex, - PrivateCells[cell].ColumnSpan); + _cellCache[cell].ColumnIndex, + _cellCache[cell].ColumnSpan); } if (forceInfinityV) { cellMeasureHeight = double.PositiveInfinity; } - else if (PrivateCells[cell].IsAutoV - && !PrivateCells[cell].IsStarV) + else if (_cellCache[cell].IsAutoV + && !_cellCache[cell].IsStarV) { // if cell belongs to at least one Auto row and not a single Star row // then it should be calculated "to content", thus it is possible to "shortcut" @@ -920,8 +909,8 @@ namespace Avalonia.Controls { cellMeasureHeight = GetMeasureSizeForRange( DefinitionsV, - PrivateCells[cell].RowIndex, - PrivateCells[cell].RowSpan); + _cellCache[cell].RowIndex, + _cellCache[cell].RowSpan); } var child = Children[cell]; @@ -1006,7 +995,7 @@ namespace Avalonia.Controls Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Length); // avoid processing when asked to distribute "0" - if (!_IsZero(requestedSize)) + if (!MathUtilities.IsZero(requestedSize)) { DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting int end = start + count; @@ -1084,7 +1073,7 @@ namespace Avalonia.Controls } // sanity check: requested size must all be distributed - Debug.Assert(_IsZero(sizeToDistribute)); + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); } else if (requestedSize <= rangeMaxSize) { @@ -1124,7 +1113,7 @@ namespace Avalonia.Controls } // sanity check: requested size must all be distributed - Debug.Assert(_IsZero(sizeToDistribute)); + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); } else { @@ -1136,7 +1125,7 @@ namespace Avalonia.Controls double equalSize = requestedSize / count; if (equalSize < maxMaxSize - && !_AreClose(equalSize, maxMaxSize)) + && !MathUtilities.AreClose(equalSize, maxMaxSize)) { // equi-size is less than maximum of maxSizes. // in this case distribute so that smaller definitions grow faster than @@ -1903,7 +1892,7 @@ namespace Avalonia.Controls // and precision of floating-point computation. (However, the resulting // display is subject to anti-aliasing problems. TANSTAAFL.) - if (!_AreClose(roundedTakenSize, finalSize)) + if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) { // Compute deltas for (int i = 0; i < definitions.Length; ++i) @@ -1920,7 +1909,7 @@ namespace Avalonia.Controls if (roundedTakenSize > finalSize) { int i = definitions.Length - 1; - while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0) + while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache - dpiIncrement; @@ -1936,7 +1925,7 @@ namespace Avalonia.Controls else if (roundedTakenSize < finalSize) { int i = 0; - while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Length) + while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Length) { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache + dpiIncrement; @@ -2054,14 +2043,13 @@ namespace Avalonia.Controls /// private void SetValid() { - ExtendedData extData = ExtData; - if (extData != null) + if (rowColDefsEmpty) { - if (extData.TempDefinitions != null) + if (_tempDefinitions != null) { // TempDefinitions has to be cleared to avoid "memory leaks" - Array.Clear(extData.TempDefinitions, 0, Math.Max(DefinitionsU.Length, DefinitionsV.Length)); - extData.TempDefinitions = null; + Array.Clear(_tempDefinitions, 0, Math.Max(DefinitionsU.Length, DefinitionsV.Length)); + _tempDefinitions = null; } } } @@ -2092,15 +2080,6 @@ namespace Avalonia.Controls 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)); - } - private double RoundLayoutValue(double value, double dpiScale) { double newValue; @@ -2125,28 +2104,6 @@ namespace Avalonia.Controls return newValue; } - /// - /// 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 int ValidateColumn(AvaloniaObject o, int value) { @@ -2170,7 +2127,7 @@ namespace Avalonia.Controls private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) { - if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + if (grid.rowColDefsEmpty // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) { grid.InvalidateVisual(); @@ -2183,7 +2140,7 @@ namespace Avalonia.Controls { var grid = child.GetVisualParent() as Grid; if (grid != null - && grid.ExtData != null + && grid.rowColDefsEmpty && grid.ListenToNotifications) { grid.CellsStructureDirty = true; @@ -2232,30 +2189,29 @@ namespace Avalonia.Controls { get { - ExtendedData extData = ExtData; int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length) * 2; - if (extData.TempDefinitions == null - || extData.TempDefinitions.Length < requiredLength) + if (_tempDefinitions == null + || _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)); + _tempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(s_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); } else { - extData.TempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; - if (extData.TempDefinitions == null - || extData.TempDefinitions.Length < requiredLength) + _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) { - extData.TempDefinitions = new DefinitionBase[requiredLength]; - tempDefinitionsWeakRef.Target = extData.TempDefinitions; + _tempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = _tempDefinitions; } } } - return (extData.TempDefinitions); + return (_tempDefinitions); } } @@ -2298,106 +2254,6 @@ namespace Avalonia.Controls } } - /// - /// Private version returning array of cells. - /// - private CellCache[] PrivateCells - { - get { return (ExtData.CellCachesCollection); } - } - - /// - /// Convenience accessor to ValidCellsStructure bit flag. - /// - private bool CellsStructureDirty - { - get { return (!CheckFlagsAnd(Flags.ValidCellsStructure)); } - set { SetFlags(!value, Flags.ValidCellsStructure); } - } - - /// - /// Convenience accessor to ListenToNotifications bit flag. - /// - private bool ListenToNotifications - { - get { return (CheckFlagsAnd(Flags.ListenToNotifications)); } - set { SetFlags(value, Flags.ListenToNotifications); } - } - - /// - /// Convenience accessor to SizeToContentU bit flag. - /// - private bool SizeToContentU - { - get { return (CheckFlagsAnd(Flags.SizeToContentU)); } - set { SetFlags(value, Flags.SizeToContentU); } - } - - /// - /// Convenience accessor to SizeToContentV bit flag. - /// - private bool SizeToContentV - { - get { return (CheckFlagsAnd(Flags.SizeToContentV)); } - set { SetFlags(value, Flags.SizeToContentV); } - } - - /// - /// Convenience accessor to HasStarCellsU bit flag. - /// - private bool HasStarCellsU - { - get { return (CheckFlagsAnd(Flags.HasStarCellsU)); } - set { SetFlags(value, Flags.HasStarCellsU); } - } - - /// - /// Convenience accessor to HasStarCellsV bit flag. - /// - private bool HasStarCellsV - { - get { return (CheckFlagsAnd(Flags.HasStarCellsV)); } - set { SetFlags(value, Flags.HasStarCellsV); } - } - - /// - /// Convenience accessor to HasGroup3CellsInAutoRows bit flag. - /// - private bool HasGroup3CellsInAutoRows - { - get { return (CheckFlagsAnd(Flags.HasGroup3CellsInAutoRows)); } - set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } - } - - /// - /// fp version of d == 0. - /// - /// Value to check. - /// true if d == 0. - private static bool _IsZero(double d) - { - return (Math.Abs(d) < double.Epsilon); - } - - /// - /// fp version of d1 == d2 - /// - /// First value to compare - /// Second value to compare - /// true if d1 == d2 - private static bool _AreClose(double d1, double d2) - { - return (Math.Abs(d1 - d2) < double.Epsilon); - } - - /// - /// Returns reference to extended data bag. - /// - private ExtendedData ExtData - { - get { return (_data); } - } - /// /// Returns *-weight, adjusted for scale computed during Phase 1 /// @@ -2416,72 +2272,6 @@ namespace Avalonia.Controls } } - private ExtendedData _data = new ExtendedData(); // 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; - private DefinitionBase[] DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - private DefinitionBase[] DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; - 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(); - - /// - /// Extended data instantiated on demand, when grid handles non-trivial case. - /// - private class ExtendedData - { - internal CellCache[] CellCachesCollection; // backing store for logical Children - internal int CellGroup1; // index of the first cell in first cell group - internal int CellGroup2; // index of the first cell in second cell group - internal int CellGroup3; // index of the first cell in third cell group - internal int CellGroup4; // index of the first cell in forth cell group - internal DefinitionBase[] TempDefinitions; // temporary array used during layout for various purposes - // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) - } - - /// - /// Grid validity / property caches dirtiness flags - /// - [System.Flags] - private enum Flags - { - // - // the foolowing flags let grid tracking dirtiness in more granular manner: - // * Valid???Structure flags indicate that elements were added or removed. - // * Valid???Layout flags indicate that layout time portion of the information - // stored on the objects should be updated. - // - ValidColumnDefinitionsStructure = 0x00000001, - ValidRowDefinitionsStructure = 0x00000002, - ValidCellsStructure = 0x00000004, - - // - // 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 - } - /// /// LayoutTimeSizeType is used internally and reflects layout-time size type. /// @@ -2663,135 +2453,6 @@ namespace Avalonia.Controls } } - /// - /// StarDistributionOrderComparer. - /// - private class StarDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// DistributionOrderComparer. - /// - private class DistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; - double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; - result = xprime.CompareTo(yprime); - } - - return result; - } - } - - - /// - /// StarDistributionOrderIndexComparer. - /// - private class StarDistributionOrderIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal StarDistributionOrderIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// DistributionOrderComparer. - /// - private class DistributionOrderIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal DistributionOrderIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - double xprime = definitionX.SizeCache - definitionX.MinSizeForArrange; - double yprime = definitionY.SizeCache - definitionY.MinSizeForArrange; - result = xprime.CompareTo(yprime); - } - - return result; - } - } - /// /// RoundingErrorIndexComparer. /// From 689ca63c7ac90c81d3eecbdcec6816f0c8bc7a6a Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 10:51:07 +0800 Subject: [PATCH 044/179] Fix DPI fallback value on LayoutRounding; Rename `rowColDefsEmpty` to `IsTrivialGrid`; --- src/Avalonia.Controls/GridWPF.cs | 66 +++++++++++++++++++------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 0549c7f20a..119c94c6e0 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -33,6 +33,11 @@ namespace Avalonia.Controls internal bool HasStarCellsU; internal bool HasStarCellsV; internal bool HasGroup3CellsInAutoRows; + internal bool MeasureOverrideInProgress; + internal bool ArrangeOverrideInProgress; + internal bool ColumnDefinitionsDirty; + internal bool RowDefinitionsDirty; + // index of the first cell in first cell group internal int CellGroup1; @@ -44,7 +49,6 @@ namespace Avalonia.Controls internal int CellGroup3; // index of the first cell in fourth cell group - internal int CellGroup4; // temporary array used during layout for various purposes @@ -60,9 +64,9 @@ namespace Avalonia.Controls // Stores unrounded values and rounding errors during layout rounding. private double[] _roundingErrors; - private DefinitionBase[] DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - private DefinitionBase[] DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; - private const int c_layoutLoopMaxCount = 5; // 5 is an arbitrary constant chosen to end the measure loop + private DefinitionBase[] DefinitionsU; + private DefinitionBase[] DefinitionsV; + private const int 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(); @@ -73,6 +77,7 @@ namespace Avalonia.Controls static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); ColumnSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); @@ -167,7 +172,6 @@ namespace Avalonia.Controls return element.GetValue(RowSpanProperty); } - /// /// Gets the value of the IsSharedSizeScope attached property for a control. /// @@ -243,6 +247,8 @@ namespace Avalonia.Controls if (_columnDefinitions.Count > 0) DefinitionsU = _columnDefinitions.Cast().ToArray(); + else + DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; _columnDefinitions.CollectionChanged += (_, e) => { @@ -283,6 +289,8 @@ namespace Avalonia.Controls if (_rowDefinitions.Count > 0) DefinitionsV = _rowDefinitions.Cast().ToArray(); + else + DefinitionsV = new DefinitionBase[1] { new ColumnDefinition() }; _rowDefinitions.CollectionChanged += (_, e) => { @@ -300,8 +308,8 @@ namespace Avalonia.Controls } } - private bool rowColDefsEmpty => (DefinitionsU.Length == 0) && - (DefinitionsV.Length == 0); + private bool IsTrivialGrid => (DefinitionsU?.Length <= 1) && + (DefinitionsV?.Length <= 1); /// /// Content measurement. @@ -317,7 +325,7 @@ namespace Avalonia.Controls ListenToNotifications = true; MeasureOverrideInProgress = true; - if (rowColDefsEmpty) + if (IsTrivialGrid) { gridDesiredSize = new Size(); @@ -358,10 +366,10 @@ namespace Avalonia.Controls } } - // ValidateColumnDefinitionsStructure(); + ValidateColumnDefinitionsStructure(); ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); - // ValidateRowDefinitionsStructure(); + ValidateRowDefinitionsStructure(); ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); @@ -429,9 +437,9 @@ namespace Avalonia.Controls ApplyCachedMinSizes(group2MinSizes, false); if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + MeasureCellsGroup(CellGroup2, constraint, cnt == layoutLoopMaxCount, false, out hasDesiredSizeUChanged); } - while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); + while (hasDesiredSizeUChanged && ++cnt <= layoutLoopMaxCount); } } } @@ -451,6 +459,18 @@ namespace Avalonia.Controls return (gridDesiredSize); } + private void ValidateColumnDefinitionsStructure() + { + if (DefinitionsU == null || DefinitionsU?.Count() == 0) + DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + } + + private void ValidateRowDefinitionsStructure() + { + if (DefinitionsV == null || DefinitionsV?.Count() == 0) + DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + } + /// /// Content arrangement. /// @@ -459,10 +479,9 @@ namespace Avalonia.Controls { try { - ArrangeOverrideInProgress = true; - if (rowColDefsEmpty) + if (IsTrivialGrid) { for (int i = 0, count = Children.Count; i < count; ++i) { @@ -527,11 +546,6 @@ namespace Avalonia.Controls InvalidateMeasure(); } - internal bool MeasureOverrideInProgress; - internal bool ArrangeOverrideInProgress; - internal bool ColumnDefinitionsDirty; - internal bool RowDefinitionsDirty; - /// /// Lays out cells according to rows and columns, and creates lookup grids. /// @@ -1638,9 +1652,9 @@ namespace Avalonia.Controls double remainingAvailableSize = finalSize - takenSize; double remainingStarWeight = totalStarWeight - takenStarWeight; - MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer((DefinitionBase[])definitions); + MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); - MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer((DefinitionBase[])definitions); + MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) @@ -1835,7 +1849,7 @@ namespace Avalonia.Controls // unrounded sizes, to avoid breaking assumptions in the previous phases if (UseLayoutRounding) { - var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 96; + var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; double[] roundingErrors = RoundingErrors; double roundedTakenSize = 0.0; @@ -2043,7 +2057,7 @@ namespace Avalonia.Controls /// private void SetValid() { - if (rowColDefsEmpty) + if (IsTrivialGrid) { if (_tempDefinitions != null) { @@ -2127,7 +2141,7 @@ namespace Avalonia.Controls private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) { - if (grid.rowColDefsEmpty // trivial grid is 1 by 1. there is no grid lines anyway + if (!grid.IsTrivialGrid // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) { grid.InvalidateVisual(); @@ -2140,7 +2154,7 @@ namespace Avalonia.Controls { var grid = child.GetVisualParent() as Grid; if (grid != null - && grid.rowColDefsEmpty + && !grid.IsTrivialGrid && grid.ListenToNotifications) { grid.CellsStructureDirty = true; @@ -2153,7 +2167,7 @@ namespace Avalonia.Controls /// Helper for Comparer methods. /// /// - /// true iff one or both of x and y are null, in which case result holds + /// true if one or both of x and y are null, in which case result holds /// the relative sort order. /// private static bool CompareNullRefs(object x, object y, out int result) From 505ed6b0cf9cacb107ab6baedae45c2d2692b24b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 11:15:04 +0800 Subject: [PATCH 045/179] Replace per-property class handlers with `AffectsParentMeasure<>`; Fix `GridLinesRenderer`. --- src/Avalonia.Controls/GridWPF.cs | 39 ++++++++------------------------ 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 119c94c6e0..f1cf1e8f5b 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -38,7 +38,6 @@ namespace Avalonia.Controls internal bool ColumnDefinitionsDirty; internal bool RowDefinitionsDirty; - // index of the first cell in first cell group internal int CellGroup1; @@ -77,11 +76,7 @@ namespace Avalonia.Controls static Grid() { 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); } /// @@ -521,12 +516,12 @@ namespace Avalonia.Controls cell.Arrange(cellRect); } - // // update render bound on grid lines renderer visual - // var gridLinesRenderer = EnsureGridLinesRenderer(); - // if (gridLinesRenderer != null) - // { - // gridLinesRenderer.UpdateRenderBounds(arrangeSize); - // } + // update render bound on grid lines renderer visual + var gridLinesRenderer = EnsureGridLinesRenderer(); + if (gridLinesRenderer != null) + { + gridLinesRenderer.UpdateRenderBounds(arrangeSize); + } } } finally @@ -2144,22 +2139,7 @@ namespace Avalonia.Controls if (!grid.IsTrivialGrid // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) { - grid.InvalidateVisual(); - } - } - - private static void OnCellAttachedPropertyChanged(Visual child, AvaloniaPropertyChangedEventArgs e) - { - if (child != null) - { - var grid = child.GetVisualParent() as Grid; - if (grid != null - && !grid.IsTrivialGrid - && grid.ListenToNotifications) - { - grid.CellsStructureDirty = true; - grid.InvalidateMeasure(); - } + grid.Invalidate(); } } @@ -2692,7 +2672,7 @@ namespace Avalonia.Controls /// /// Helper to render grid lines. /// - internal class GridLinesRenderer : Visual + internal class GridLinesRenderer : Control { /// /// Static initialization @@ -2769,6 +2749,7 @@ namespace Avalonia.Controls lastArrangeSize = arrangeSize; this.InvalidateVisual(); } + private static Size lastArrangeSize; private const double c_dashLength = 4.0; // private const double c_penWidth = 1.0; // From 4c1f47695773da777b984cf69032c7904a802368 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 11:35:58 +0800 Subject: [PATCH 046/179] Rename private fields, remove practically unused fields. --- src/Avalonia.Controls/GridWPF.cs | 55 +++++++++++++------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index f1cf1e8f5b..334d57ecf8 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -27,14 +27,11 @@ namespace Avalonia.Controls public class Grid : Panel { internal bool CellsStructureDirty = true; - internal bool ListenToNotifications; internal bool SizeToContentU; internal bool SizeToContentV; internal bool HasStarCellsU; internal bool HasStarCellsV; internal bool HasGroup3CellsInAutoRows; - internal bool MeasureOverrideInProgress; - internal bool ArrangeOverrideInProgress; internal bool ColumnDefinitionsDirty; internal bool RowDefinitionsDirty; @@ -65,13 +62,15 @@ namespace Avalonia.Controls private double[] _roundingErrors; private DefinitionBase[] DefinitionsU; private DefinitionBase[] DefinitionsV; - private const int 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_minRatioComparer = new MinRatioComparer(); - private static readonly IComparer s_maxRatioComparer = new MaxRatioComparer(); - private static readonly IComparer s_starWeightComparer = new StarWeightComparer(); + + // 5 is an arbitrary constant chosen to end the measure loop + private const int _layoutLoopMaxCount = 5; + private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + private static readonly IComparer _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + private static readonly IComparer _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + private static readonly IComparer _minRatioComparer = new MinRatioComparer(); + private static readonly IComparer _maxRatioComparer = new MaxRatioComparer(); + private static readonly IComparer _starWeightComparer = new StarWeightComparer(); static Grid() { @@ -317,9 +316,6 @@ namespace Avalonia.Controls try { - ListenToNotifications = true; - MeasureOverrideInProgress = true; - if (IsTrivialGrid) { gridDesiredSize = new Size(); @@ -432,9 +428,9 @@ namespace Avalonia.Controls ApplyCachedMinSizes(group2MinSizes, false); if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, cnt == layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + MeasureCellsGroup(CellGroup2, constraint, cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); } - while (hasDesiredSizeUChanged && ++cnt <= layoutLoopMaxCount); + while (hasDesiredSizeUChanged && ++cnt <= _layoutLoopMaxCount); } } } @@ -448,7 +444,6 @@ namespace Avalonia.Controls } finally { - MeasureOverrideInProgress = false; } return (gridDesiredSize); @@ -474,8 +469,6 @@ namespace Avalonia.Controls { try { - ArrangeOverrideInProgress = true; - if (IsTrivialGrid) { for (int i = 0, count = Children.Count; i < count; ++i) @@ -527,7 +520,6 @@ namespace Avalonia.Controls finally { SetValid(); - ArrangeOverrideInProgress = false; } return (arrangeSize); } @@ -1061,7 +1053,7 @@ namespace Avalonia.Controls double sizeToDistribute; int i; - Array.Sort(tempDefinitions, 0, count, s_spanPreferredDistributionOrderComparer); + Array.Sort(tempDefinitions, 0, count, _spanPreferredDistributionOrderComparer); for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) { // sanity check: only auto definitions allowed in this loop @@ -1098,7 +1090,7 @@ namespace Avalonia.Controls double sizeToDistribute; int i; - Array.Sort(tempDefinitions, 0, count, s_spanMaxDistributionOrderComparer); + Array.Sort(tempDefinitions, 0, count, _spanMaxDistributionOrderComparer); for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) { // sanity check: no auto definitions allowed in this loop @@ -1301,8 +1293,8 @@ namespace Avalonia.Controls 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); + Array.Sort(tempDefinitions, 0, minCount, _minRatioComparer); + Array.Sort(tempDefinitions, defCount, maxCount, _maxRatioComparer); while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) { @@ -1458,7 +1450,7 @@ namespace Avalonia.Controls if (starCount > 0) { - Array.Sort(tempDefinitions, 0, starCount, s_starWeightComparer); + Array.Sort(tempDefinitions, 0, starCount, _starWeightComparer); // compute the partial sums of *-weight, in increasing order of weight // for minimal loss of precision. @@ -2136,8 +2128,7 @@ namespace Avalonia.Controls private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) { - if (!grid.IsTrivialGrid // trivial grid is 1 by 1. there is no grid lines anyway - && grid.ListenToNotifications) + if (!grid.IsTrivialGrid) // trivial grid is 1 by 1. there is no grid lines anyway { grid.Invalidate(); } @@ -2188,11 +2179,11 @@ namespace Avalonia.Controls if (_tempDefinitions == null || _tempDefinitions.Length < requiredLength) { - WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(s_tempDefinitionsDataSlot); + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); if (tempDefinitionsWeakRef == null) { _tempDefinitions = new DefinitionBase[requiredLength]; - Thread.SetData(s_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); + Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); } else { @@ -2672,7 +2663,7 @@ namespace Avalonia.Controls /// /// Helper to render grid lines. /// - internal class GridLinesRenderer : Control + private class GridLinesRenderer : Control { /// /// Static initialization @@ -2703,10 +2694,11 @@ namespace Avalonia.Controls /// public override void Render(DrawingContext drawingContext) { - var grid = this.GetVisualParent() as Grid; + var grid = this.GetVisualParent(); if (grid == null - || grid.ShowGridLines == false) + || !grid.ShowGridLines + || grid.IsTrivialGrid) { return; } @@ -2755,7 +2747,6 @@ namespace Avalonia.Controls private const double c_penWidth = 1.0; // private static readonly Pen s_oddDashPen; // first pen to draw dash private static readonly Pen s_evenDashPen; // second pen to draw dash - private static readonly Point c_zeroPoint = new Point(0, 0); } } } \ No newline at end of file From 69c056c728ffa6a504aeedc25c57cbdc7b532f92 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 12:08:14 +0800 Subject: [PATCH 047/179] Downsize DefinitionBase, removing most of shared size scope code. --- src/Avalonia.Controls/ColumnDefinition.cs | 6 +- src/Avalonia.Controls/DefinitionBase.cs | 517 +--------------------- src/Avalonia.Controls/GridWPF.cs | 50 +-- src/Avalonia.Controls/RowDefinition.cs | 6 +- 4 files changed, 49 insertions(+), 530 deletions(-) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index 015484dbcc..56a6d41b7b 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -88,10 +88,10 @@ namespace Avalonia.Controls set { SetValue(WidthProperty, value); } } - internal override GridLength UserSizeValueCache => this.Width; + internal override GridLength UserSize => this.Width; - internal override double UserMinSizeValueCache => this.MinWidth; + internal override double UserMinSize => this.MinWidth; - internal override double UserMaxSizeValueCache => this.MaxWidth; + internal override double UserMaxSize => this.MaxWidth; } } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 643d9da751..38fc375631 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -9,27 +9,16 @@ using Avalonia.Utilities; namespace Avalonia.Controls { - /// /// Base class for and . /// - public abstract class DefinitionBase : ContentControl + public abstract class DefinitionBase : AvaloniaObject { - /// - /// Static ctor. Used for static registration of properties. - /// - static DefinitionBase() - { - SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); - // BoundsProperty.Changed.AddClassHandler(OnUserSizePropertyChanged); - } - - /// /// Defines the property. /// public static readonly StyledProperty SharedSizeGroupProperty = - AvaloniaProperty.Register(nameof(SharedSizeGroup), inherits: true, validate: SharedSizeGroupPropertyValueValid); + AvaloniaProperty.Register(nameof(SharedSizeGroup), inherits: true); /// /// Gets or sets the name of the shared size group of the column or row. @@ -41,84 +30,30 @@ namespace Avalonia.Controls } /// - /// 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); } - } - - - /// - /// 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 helper to access up-to-date UserSize property value. /// - internal GridLength UserSize - { - get { return (_sharedState != null ? _sharedState.UserSize : UserSizeValueCache); } - } + internal abstract GridLength UserSize { get; } /// - /// Internal accessor to user min size field. - /// - internal double UserMinSize - { - get { return (UserMinSizeValueCache); } - } - - /// - /// Internal accessor to user max size field. + /// Internal helper to access up-to-date UserMinSize property value. /// - internal double UserMaxSize - { - get { return (UserMaxSizeValueCache); } - } + internal abstract double UserMinSize { get; } /// - /// DefinitionBase's index in the parents collection. + /// Internal helper to access up-to-date UserMaxSize property value. /// - internal int Index - { - get - { - return (_parentIndex); - } - set - { - Debug.Assert(value >= -1 && _parentIndex != value); - _parentIndex = value; - } - } + internal abstract double UserMaxSize { get; } + + private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's /// /// Layout-time user size type. /// - internal Grid.LayoutTimeSizeType SizeType - { - get { return (_sizeType); } - set { _sizeType = value; } - } - + internal Grid.LayoutTimeSizeType SizeType {get; set;} /// /// Returns or sets measure size for the definition. /// - internal double MeasureSize - { - get { return (_measureSize); } - set { _measureSize = value; } - } + internal double MeasureSize { get; set; } /// /// Returns definition's layout time type sensitive preferred size. @@ -131,10 +66,10 @@ namespace Avalonia.Controls get { double preferredSize = MinSize; - if (_sizeType != Grid.LayoutTimeSizeType.Auto - && preferredSize < _measureSize) + if (SizeType != Grid.LayoutTimeSizeType.Auto + && preferredSize < MeasureSize) { - preferredSize = _measureSize; + preferredSize = MeasureSize; } return (preferredSize); } @@ -143,11 +78,7 @@ namespace Avalonia.Controls /// /// Returns or sets size cache for the definition. /// - internal double SizeCache - { - get { return (_sizeCache); } - set { _sizeCache = value; } - } + internal double SizeCache { get; set; } /// /// Returns min size. @@ -157,12 +88,6 @@ namespace Avalonia.Controls get { double minSize = _minSize; - if (UseSharedMinimum - && _sharedState != null - && minSize < _sharedState.MinSize) - { - minSize = _sharedState.MinSize; - } return (minSize); } @@ -194,12 +119,6 @@ namespace Avalonia.Controls get { double minSize = _minSize; - if (_sharedState != null - && (UseSharedMinimum || !LayoutWasUpdated) - && minSize < _sharedState.MinSize) - { - minSize = _sharedState.MinSize; - } return (minSize); } } @@ -207,409 +126,11 @@ namespace Avalonia.Controls /// /// 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); } - } - - private static void OnSharedSizeGroupPropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) - { - 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(DefinitionBase _, string value) - { - // null is default value - if (value == null) - { - return value; - } - - 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; - } - } - - return null; - } - - /// - /// - /// - /// - /// 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); - } - } - } - } - - /// - /// Convenience accessor to UseSharedMinimum flag - /// - private bool UseSharedMinimum - { - get { return _useSharedMinimum; } - set { _useSharedMinimum = value; } - } + internal double FinalOffset { get; set; } /// - /// Convenience accessor to LayoutWasUpdated flag - /// - private bool LayoutWasUpdated - { - get { return _layoutWasUpdated; } - set { _layoutWasUpdated = value; } - } - - private 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 - private bool _layoutWasUpdated; - private bool _useSharedMinimum; - - - - /// - /// Collection of shared states objects for a single scope - /// - private 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 + /// Returns true if this definition is a part of shared group. /// - private 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(IControl layoutUpdatedHost) - { - if (_layoutUpdatedHost == null) - { - _layoutUpdatedHost = layoutUpdatedHost; - // PORTING HACK... Remove this when resolved. - _layoutUpdatedHost.GetSubject(Visual.BoundsProperty).Subscribe(p => _layoutUpdated?.Invoke(this, null)); - } - } - - /// - /// 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 IControl _layoutUpdatedHost; // IControl 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 - } + internal bool IsShared { get; set; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 334d57ecf8..e18d70c840 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -649,7 +649,7 @@ namespace Avalonia.Controls { for (int i = 0; i < definitions.Length; ++i) { - definitions[i].OnBeforeLayout(this); + // definitions[i].OnBeforeLayout(this); double userMinSize = definitions[i].UserMinSize; double userMaxSize = definitions[i].UserMaxSize; @@ -1215,17 +1215,17 @@ namespace Avalonia.Controls } } - if (Double.IsPositiveInfinity(maxStar)) + 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 + // 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)); + 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 @@ -1277,7 +1277,7 @@ namespace Avalonia.Controls } double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); - if (!Double.IsPositiveInfinity(effectiveMaxSize)) + if (!double.IsPositiveInfinity(effectiveMaxSize)) { // store ratio w/max in SizeCache (for now) tempDefinitions[defCount + maxCount++] = def; @@ -1322,7 +1322,7 @@ namespace Avalonia.Controls remainingStarWeight = totalStarWeight - takenStarWeight; } - double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : Double.PositiveInfinity; + 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") @@ -1532,17 +1532,17 @@ namespace Avalonia.Controls } } - if (Double.IsPositiveInfinity(maxStar)) + 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 + // 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)); + 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 @@ -1590,7 +1590,7 @@ namespace Avalonia.Controls } double effectiveMaxSize = Math.Max(def.MinSizeForArrange, def.UserMaxSize); - if (!Double.IsPositiveInfinity(effectiveMaxSize)) + if (!double.IsPositiveInfinity(effectiveMaxSize)) { // store ratio w/max in SizeCache (for now) definitionIndices[defCount + maxCount++] = i; @@ -1670,7 +1670,7 @@ namespace Avalonia.Controls remainingStarWeight = totalStarWeight - takenStarWeight; } - double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : Double.PositiveInfinity; + 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") @@ -1950,19 +1950,17 @@ namespace Avalonia.Controls } } - /// - /// 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) - /// + // 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) @@ -2092,7 +2090,7 @@ namespace Avalonia.Controls // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. if (double.IsNaN(newValue) || double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, Double.MaxValue)) + MathUtilities.AreClose(newValue, double.MaxValue)) { newValue = value; } @@ -2249,7 +2247,7 @@ namespace Avalonia.Controls // 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; + return (double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; } else { diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/RowDefinition.cs index 1cb09e16e9..d9120b010e 100644 --- a/src/Avalonia.Controls/RowDefinition.cs +++ b/src/Avalonia.Controls/RowDefinition.cs @@ -89,10 +89,10 @@ namespace Avalonia.Controls } - internal override GridLength UserSizeValueCache => this.Height; + internal override GridLength UserSize => this.Height; - internal override double UserMinSizeValueCache => this.MinHeight; + internal override double UserMinSize => this.MinHeight; - internal override double UserMaxSizeValueCache => this.MaxHeight; + internal override double UserMaxSize => this.MaxHeight; } } \ No newline at end of file From 998149c9e5732befb39c622ca2b599b66db28975 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 13:31:38 +0800 Subject: [PATCH 048/179] Calculate ActualWidth/Height for Row/ColDefinitions so that GridSplitter can work. --- src/Avalonia.Controls/GridWPF.cs | 78 ++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index e18d70c840..aa08dd8469 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -32,8 +32,8 @@ namespace Avalonia.Controls internal bool HasStarCellsU; internal bool HasStarCellsV; internal bool HasGroup3CellsInAutoRows; - internal bool ColumnDefinitionsDirty; - internal bool RowDefinitionsDirty; + internal bool ColumnDefinitionsDirty = true; + internal bool RowDefinitionsDirty = true; // index of the first cell in first cell group internal int CellGroup1; @@ -284,13 +284,13 @@ namespace Avalonia.Controls if (_rowDefinitions.Count > 0) DefinitionsV = _rowDefinitions.Cast().ToArray(); else - DefinitionsV = new DefinitionBase[1] { new ColumnDefinition() }; + DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; _rowDefinitions.CollectionChanged += (_, e) => { if (_rowDefinitions.Count == 0) { - DefinitionsV = new DefinitionBase[1] { new ColumnDefinition() }; + DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; } else { @@ -451,14 +451,23 @@ namespace Avalonia.Controls private void ValidateColumnDefinitionsStructure() { - if (DefinitionsU == null || DefinitionsU?.Count() == 0) - DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + if (ColumnDefinitionsDirty) + { + if (DefinitionsU == null) + DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + ColumnDefinitionsDirty = false; + } } private void ValidateRowDefinitionsStructure() { - if (DefinitionsV == null || DefinitionsV?.Count() == 0) - DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + if (RowDefinitionsDirty) + { + if (DefinitionsV == null) + DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + + RowDefinitionsDirty = false; + } } /// @@ -521,11 +530,60 @@ namespace Avalonia.Controls { SetValid(); } + + for (var i = 0; i < ColumnDefinitions.Count; i++) + { + ColumnDefinitions[i].ActualWidth = GetFinalColumnDefinitionWidth(i); + } + + for (var i = 0; i < RowDefinitions.Count; i++) + { + RowDefinitions[i].ActualHeight = GetFinalRowDefinitionHeight(i); + } + return (arrangeSize); } /// - /// Invalidates grid caches and makes the grid dirty for measure. + /// Returns final width for a column. + /// + /// + /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. + /// + internal double GetFinalColumnDefinitionWidth(int columnIndex) + { + double value = 0.0; + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionsDirty) + { + value = DefinitionsU[(columnIndex + 1) % DefinitionsU.Length].FinalOffset; + if (columnIndex != 0) { value -= DefinitionsU[columnIndex].FinalOffset; } + } + return (value); + } + + /// + /// Returns final height for a row. + /// + /// + /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. + /// + internal double GetFinalRowDefinitionHeight(int rowIndex) + { + double value = 0.0; + + // actual value calculations require structure to be up-to-date + if (!RowDefinitionsDirty) + { + value = DefinitionsV[(rowIndex + 1) % DefinitionsV.Length].FinalOffset; + if (rowIndex != 0) { value -= DefinitionsV[rowIndex].FinalOffset; } + } + return (value); + } + + /// + /// Invalidates grid caches and makes the grid dirty for measure. /// internal void Invalidate() { @@ -2695,7 +2753,7 @@ namespace Avalonia.Controls var grid = this.GetVisualParent(); if (grid == null - || !grid.ShowGridLines + || !grid.ShowGridLines || grid.IsTrivialGrid) { return; From cc3422b4c39ea2cf52e14bf23ec02664a41fcf2a Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 15:08:40 +0800 Subject: [PATCH 049/179] Reset MinSize on DefinitionsLayout validation. --- src/Avalonia.Controls/GridWPF.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index aa08dd8469..d86ec4c1c0 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -707,7 +707,8 @@ namespace Avalonia.Controls { for (int i = 0; i < definitions.Length; ++i) { - // definitions[i].OnBeforeLayout(this); + // Reset minimum size. + definitions[i].SetMinSize(0); double userMinSize = definitions[i].UserMinSize; double userMaxSize = definitions[i].UserMaxSize; @@ -718,6 +719,7 @@ namespace Avalonia.Controls 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; From b6d01fcaeb7fefc2fd88dd50304beb398b4c1274 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 15:11:21 +0800 Subject: [PATCH 050/179] Cleanup MinSize. --- src/Avalonia.Controls/DefinitionBase.cs | 39 +++---------------------- src/Avalonia.Controls/GridWPF.cs | 28 +++++++++--------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 38fc375631..3301d23f61 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -43,13 +43,11 @@ namespace Avalonia.Controls /// Internal helper to access up-to-date UserMaxSize property value. /// internal abstract double UserMaxSize { get; } - - private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's /// /// Layout-time user size type. /// - internal Grid.LayoutTimeSizeType SizeType {get; set;} + internal Grid.LayoutTimeSizeType SizeType { get; set; } /// /// Returns or sets measure size for the definition. /// @@ -81,17 +79,9 @@ namespace Avalonia.Controls internal double SizeCache { get; set; } /// - /// Returns min size. + /// Used during measure to accumulate size for "Auto" and "Star" DefinitionBase's /// - internal double MinSize - { - get - { - double minSize = _minSize; - return (minSize); - } - - } + internal double MinSize { get; set; } /// /// Updates min size. @@ -99,28 +89,7 @@ namespace Avalonia.Controls /// New size. internal void UpdateMinSize(double minSize) { - _minSize = Math.Max(_minSize, minSize); - } - - /// - /// Sets min size. - /// - /// New size. - internal void SetMinSize(double minSize) - { - _minSize = minSize; - } - - /// - /// Returns min size, always taking into account shared state. - /// - internal double MinSizeForArrange - { - get - { - double minSize = _minSize; - return (minSize); - } + MinSize = Math.Max(MinSize, minSize); } /// diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index d86ec4c1c0..970b4de865 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -708,7 +708,7 @@ namespace Avalonia.Controls for (int i = 0; i < definitions.Length; ++i) { // Reset minimum size. - definitions[i].SetMinSize(0); + definitions[i].MinSize = 0; double userMinSize = definitions[i].UserMinSize; double userMaxSize = definitions[i].UserMaxSize; @@ -719,7 +719,7 @@ namespace Avalonia.Controls 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; @@ -784,11 +784,11 @@ namespace Avalonia.Controls { if (isRows) { - DefinitionsV[i].SetMinSize(minSizes[i]); + DefinitionsV[i].MinSize = minSizes[i]; } else { - DefinitionsU[i].SetMinSize(minSizes[i]); + DefinitionsU[i].MinSize = minSizes[i]; } } } @@ -1642,14 +1642,14 @@ namespace Avalonia.Controls double starWeight = StarWeight(def, scale); totalStarWeight += starWeight; - if (def.MinSizeForArrange > 0.0) + if (def.MinSize > 0.0) { // store ratio w/min in MeasureSize (for now) definitionIndices[minCount++] = i; - def.MeasureSize = starWeight / def.MinSizeForArrange; + def.MeasureSize = starWeight / def.MinSize; } - double effectiveMaxSize = Math.Max(def.MinSizeForArrange, def.UserMaxSize); + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); if (!double.IsPositiveInfinity(effectiveMaxSize)) { // store ratio w/max in SizeCache (for now) @@ -1669,7 +1669,7 @@ namespace Avalonia.Controls break; case (GridUnitType.Auto): - userSize = def.MinSizeForArrange; + userSize = def.MinSize; break; } @@ -1688,7 +1688,7 @@ namespace Avalonia.Controls userMaxSize = def.UserMaxSize; } - def.SizeCache = Math.Max(def.MinSizeForArrange, Math.Min(userSize, userMaxSize)); + def.SizeCache = Math.Max(def.MinSize, Math.Min(userSize, userMaxSize)); takenSize += def.SizeCache; } } @@ -1752,14 +1752,14 @@ namespace Avalonia.Controls { resolvedIndex = definitionIndices[minCount - 1]; resolvedDef = definitions[resolvedIndex]; - resolvedSize = resolvedDef.MinSizeForArrange; + resolvedSize = resolvedDef.MinSize; --minCount; } else { resolvedIndex = definitionIndices[defCount + maxCount - 1]; resolvedDef = definitions[resolvedIndex]; - resolvedSize = Math.Max(resolvedDef.MinSizeForArrange, resolvedDef.UserMaxSize); + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); --maxCount; } @@ -1882,7 +1882,7 @@ namespace Avalonia.Controls // 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); + resolvedSize = Math.Max(def.MinSize, resolvedSize); // Use the raw (unrounded) sizes to update takenSize, so that // proportions are computed in the same terms as in phase 3; @@ -1974,7 +1974,7 @@ namespace Avalonia.Controls { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache - dpiIncrement; - final = Math.Max(final, definition.MinSizeForArrange); + final = Math.Max(final, definition.MinSize); if (final < definition.SizeCache) { adjustedSize -= dpiIncrement; @@ -1990,7 +1990,7 @@ namespace Avalonia.Controls { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache + dpiIncrement; - final = Math.Max(final, definition.MinSizeForArrange); + final = Math.Max(final, definition.MinSize); if (final > definition.SizeCache) { adjustedSize += dpiIncrement; From 6d2601b54c581fa5bd5a332c84270d77ea333c78 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 17:16:45 +0800 Subject: [PATCH 051/179] Some refactoring... --- src/Avalonia.Controls/DefinitionBase.cs | 8 +- src/Avalonia.Controls/GridWPF.cs | 182 +++++++++++++----------- 2 files changed, 100 insertions(+), 90 deletions(-) diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 3301d23f61..3dd4819ca7 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -48,6 +48,7 @@ namespace Avalonia.Controls /// Layout-time user size type. /// internal Grid.LayoutTimeSizeType SizeType { get; set; } + /// /// Returns or sets measure size for the definition. /// @@ -79,7 +80,7 @@ namespace Avalonia.Controls internal double SizeCache { get; set; } /// - /// Used during measure to accumulate size for "Auto" and "Star" DefinitionBase's + /// Used during measure and arrange to accumulate size for "Auto" and "Star" DefinitionBase's /// internal double MinSize { get; set; } @@ -96,10 +97,5 @@ namespace Avalonia.Controls /// Offset. /// internal double FinalOffset { get; set; } - - /// - /// Returns true if this definition is a part of shared group. - /// - internal bool IsShared { get; set; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs index 970b4de865..35a38a6423 100644 --- a/src/Avalonia.Controls/GridWPF.cs +++ b/src/Avalonia.Controls/GridWPF.cs @@ -60,22 +60,29 @@ namespace Avalonia.Controls // Stores unrounded values and rounding errors during layout rounding. private double[] _roundingErrors; - private DefinitionBase[] DefinitionsU; - private DefinitionBase[] DefinitionsV; + private DefinitionBase[] _definitionsU; + private DefinitionBase[] _definitionsV; // 5 is an arbitrary constant chosen to end the measure loop private const int _layoutLoopMaxCount = 5; - private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); - private static readonly IComparer _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); - private static readonly IComparer _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); - private static readonly IComparer _minRatioComparer = new MinRatioComparer(); - private static readonly IComparer _maxRatioComparer = new MaxRatioComparer(); - private static readonly IComparer _starWeightComparer = new StarWeightComparer(); + private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot; + private static readonly IComparer _spanPreferredDistributionOrderComparer; + private static readonly IComparer _spanMaxDistributionOrderComparer; + private static readonly IComparer _minRatioComparer; + private static readonly IComparer _maxRatioComparer; + private static readonly IComparer _starWeightComparer; static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + + _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + _minRatioComparer = new MinRatioComparer(); + _maxRatioComparer = new MaxRatioComparer(); + _starWeightComparer = new StarWeightComparer(); } /// @@ -240,19 +247,19 @@ namespace Avalonia.Controls ColumnDefinitionsDirty = true; if (_columnDefinitions.Count > 0) - DefinitionsU = _columnDefinitions.Cast().ToArray(); + _definitionsU = _columnDefinitions.Cast().ToArray(); else - DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; _columnDefinitions.CollectionChanged += (_, e) => { if (_columnDefinitions.Count == 0) { - DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; } else { - DefinitionsU = _columnDefinitions.Cast().ToArray(); + _definitionsU = _columnDefinitions.Cast().ToArray(); ColumnDefinitionsDirty = true; } Invalidate(); @@ -282,19 +289,19 @@ namespace Avalonia.Controls RowDefinitionsDirty = true; if (_rowDefinitions.Count > 0) - DefinitionsV = _rowDefinitions.Cast().ToArray(); + _definitionsV = _rowDefinitions.Cast().ToArray(); else - DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; _rowDefinitions.CollectionChanged += (_, e) => { if (_rowDefinitions.Count == 0) { - DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; } else { - DefinitionsV = _rowDefinitions.Cast().ToArray(); + _definitionsV = _rowDefinitions.Cast().ToArray(); RowDefinitionsDirty = true; } Invalidate(); @@ -302,8 +309,8 @@ namespace Avalonia.Controls } } - private bool IsTrivialGrid => (DefinitionsU?.Length <= 1) && - (DefinitionsV?.Length <= 1); + private bool IsTrivialGrid => (_definitionsU?.Length <= 1) && + (_definitionsV?.Length <= 1); /// /// Content measurement. @@ -358,12 +365,13 @@ namespace Avalonia.Controls } ValidateColumnDefinitionsStructure(); - ValidateDefinitionsLayout(DefinitionsU, sizeToContentU); + ValidateDefinitionsLayout(_definitionsU, sizeToContentU); ValidateRowDefinitionsStructure(); - ValidateDefinitionsLayout(DefinitionsV, sizeToContentV); + ValidateDefinitionsLayout(_definitionsV, sizeToContentV); - CellsStructureDirty |= (SizeToContentU != sizeToContentU) || (SizeToContentV != sizeToContentV); + CellsStructureDirty |= (SizeToContentU != sizeToContentU) + || (SizeToContentV != sizeToContentV); SizeToContentU = sizeToContentU; SizeToContentV = sizeToContentV; @@ -371,7 +379,7 @@ namespace Avalonia.Controls ValidateCells(); - Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); + Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); MeasureCellsGroup(CellGroup1, constraint, false, false); @@ -381,9 +389,9 @@ namespace Avalonia.Controls if (canResolveStarsV) { - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } MeasureCellsGroup(CellGroup2, constraint, false, false); - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } MeasureCellsGroup(CellGroup3, constraint, false, false); } else @@ -393,9 +401,9 @@ namespace Avalonia.Controls bool canResolveStarsU = CellGroup2 > _cellCache.Length; if (canResolveStarsU) { - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } MeasureCellsGroup(CellGroup3, constraint, false, false); - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } } else { @@ -421,14 +429,15 @@ namespace Avalonia.Controls ApplyCachedMinSizes(group3MinSizes, true); } - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } MeasureCellsGroup(CellGroup3, constraint, false, false); // Reset cached Group2Widths ApplyCachedMinSizes(group2MinSizes, false); - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + MeasureCellsGroup(CellGroup2, constraint, + cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); } while (hasDesiredSizeUChanged && ++cnt <= _layoutLoopMaxCount); } @@ -438,8 +447,8 @@ namespace Avalonia.Controls MeasureCellsGroup(CellGroup4, constraint, false, false); gridDesiredSize = new Size( - CalculateDesiredSize(DefinitionsU), - CalculateDesiredSize(DefinitionsV)); + CalculateDesiredSize(_definitionsU), + CalculateDesiredSize(_definitionsV)); } } finally @@ -453,8 +462,8 @@ namespace Avalonia.Controls { if (ColumnDefinitionsDirty) { - if (DefinitionsU == null) - DefinitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + if (_definitionsU == null) + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; ColumnDefinitionsDirty = false; } } @@ -463,8 +472,8 @@ namespace Avalonia.Controls { if (RowDefinitionsDirty) { - if (DefinitionsV == null) - DefinitionsV = new DefinitionBase[1] { new RowDefinition() }; + if (_definitionsV == null) + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; RowDefinitionsDirty = false; } @@ -491,10 +500,10 @@ namespace Avalonia.Controls } else { - Debug.Assert(DefinitionsU.Length > 0 && DefinitionsV.Length > 0); + Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); - SetFinalSize(DefinitionsU, arrangeSize.Width, true); - SetFinalSize(DefinitionsV, arrangeSize.Height, false); + SetFinalSize(_definitionsU, arrangeSize.Width, true); + SetFinalSize(_definitionsV, arrangeSize.Height, false); for (int currentCell = 0; currentCell < _cellCache.Length; ++currentCell) { @@ -510,10 +519,10 @@ namespace Avalonia.Controls int rowSpan = _cellCache[currentCell].RowSpan; Rect cellRect = new Rect( - columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, - rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, - GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), - GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); + 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); } @@ -557,8 +566,8 @@ namespace Avalonia.Controls // actual value calculations require structure to be up-to-date if (!ColumnDefinitionsDirty) { - value = DefinitionsU[(columnIndex + 1) % DefinitionsU.Length].FinalOffset; - if (columnIndex != 0) { value -= DefinitionsU[columnIndex].FinalOffset; } + value = _definitionsU[(columnIndex + 1) % _definitionsU.Length].FinalOffset; + if (columnIndex != 0) { value -= _definitionsU[columnIndex].FinalOffset; } } return (value); } @@ -576,8 +585,8 @@ namespace Avalonia.Controls // actual value calculations require structure to be up-to-date if (!RowDefinitionsDirty) { - value = DefinitionsV[(rowIndex + 1) % DefinitionsV.Length].FinalOffset; - if (rowIndex != 0) { value -= DefinitionsV[rowIndex].FinalOffset; } + value = _definitionsV[(rowIndex + 1) % _definitionsV.Length].FinalOffset; + if (rowIndex != 0) { value -= _definitionsV[rowIndex].FinalOffset; } } return (value); } @@ -622,29 +631,29 @@ namespace Avalonia.Controls // read indices from the corresponding properties // clamp to value < number_of_columns // column >= 0 is guaranteed by property value validation callback - cell.ColumnIndex = Math.Min(GetColumn(child), DefinitionsU.Length - 1); + cell.ColumnIndex = Math.Min(GetColumn(child), _definitionsU.Length - 1); + // clamp to value < number_of_rows // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Length - 1); + cell.RowIndex = Math.Min(GetRow(child), _definitionsV.Length - 1); // read span properties // clamp to not exceed beyond right side of the grid // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(GetColumnSpan(child), DefinitionsU.Length - cell.ColumnIndex); + cell.ColumnSpan = Math.Min(GetColumnSpan(child), _definitionsU.Length - cell.ColumnIndex); // clamp to not exceed beyond bottom side of the grid // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Length - cell.RowIndex); + cell.RowSpan = Math.Min(GetRowSpan(child), _definitionsV.Length - cell.RowIndex); - Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Length); - Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Length); + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < _definitionsU.Length); + Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < _definitionsV.Length); // // calculate and cache length types for the child // - - cell.SizeTypeU = GetLengthTypeForRange(DefinitionsU, cell.ColumnIndex, cell.ColumnSpan); - cell.SizeTypeV = GetLengthTypeForRange(DefinitionsV, cell.RowIndex, cell.RowSpan); + cell.SizeTypeU = GetLengthTypeForRange(_definitionsU, cell.ColumnIndex, cell.ColumnSpan); + cell.SizeTypeV = GetLengthTypeForRange(_definitionsV, cell.RowIndex, cell.RowSpan); hasStarCellsU |= cell.IsStarU; hasStarCellsV |= cell.IsStarV; @@ -652,7 +661,6 @@ namespace Avalonia.Controls // // distribute cells into four groups. // - if (!cell.IsStarV) { if (!cell.IsStarU) @@ -751,7 +759,8 @@ namespace Avalonia.Controls private double[] CacheMinSizes(int cellsHead, bool isRows) { - double[] minSizes = isRows ? new double[DefinitionsV.Length] : new double[DefinitionsU.Length]; + double[] minSizes = isRows ? new double[_definitionsV.Length] + : new double[_definitionsU.Length]; for (int j = 0; j < minSizes.Length; j++) { @@ -763,11 +772,11 @@ namespace Avalonia.Controls { if (isRows) { - minSizes[_cellCache[i].RowIndex] = DefinitionsV[_cellCache[i].RowIndex].MinSize; + minSizes[_cellCache[i].RowIndex] = _definitionsV[_cellCache[i].RowIndex].MinSize; } else { - minSizes[_cellCache[i].ColumnIndex] = DefinitionsU[_cellCache[i].ColumnIndex].MinSize; + minSizes[_cellCache[i].ColumnIndex] = _definitionsU[_cellCache[i].ColumnIndex].MinSize; } i = _cellCache[i].Next; @@ -784,11 +793,11 @@ namespace Avalonia.Controls { if (isRows) { - DefinitionsV[i].MinSize = minSizes[i]; + _definitionsV[i].MinSize = minSizes[i]; } else { - DefinitionsU[i].MinSize = minSizes[i]; + _definitionsU[i].MinSize = minSizes[i]; } } } @@ -801,7 +810,8 @@ namespace Avalonia.Controls bool forceInfinityV) { bool unusedHasDesiredSizeUChanged; - MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, forceInfinityV, out unusedHasDesiredSizeUChanged); + MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, + forceInfinityV, out unusedHasDesiredSizeUChanged); } /// @@ -844,7 +854,9 @@ namespace Avalonia.Controls { if (_cellCache[i].ColumnSpan == 1) { - DefinitionsU[_cellCache[i].ColumnIndex].UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, DefinitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); + _definitionsU[_cellCache[i].ColumnIndex] + .UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, + _definitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); } else { @@ -861,7 +873,9 @@ namespace Avalonia.Controls { if (_cellCache[i].RowSpan == 1) { - DefinitionsV[_cellCache[i].RowIndex].UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, DefinitionsV[_cellCache[i].RowIndex].UserMaxSize)); + _definitionsV[_cellCache[i].RowIndex] + .UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, + _definitionsV[_cellCache[i].RowIndex].UserMaxSize)); } else { @@ -885,7 +899,7 @@ namespace Avalonia.Controls double requestedSize = (double)e.Value; EnsureMinSizeInDefinitionRange( - key.U ? DefinitionsU : DefinitionsV, + key.U ? _definitionsU : _definitionsV, key.Start, key.Count, requestedSize, @@ -949,7 +963,7 @@ namespace Avalonia.Controls { // otherwise... cellMeasureWidth = GetMeasureSizeForRange( - DefinitionsU, + _definitionsU, _cellCache[cell].ColumnIndex, _cellCache[cell].ColumnSpan); } @@ -969,7 +983,7 @@ namespace Avalonia.Controls else { cellMeasureHeight = GetMeasureSizeForRange( - DefinitionsV, + _definitionsV, _cellCache[cell].RowIndex, _cellCache[cell].RowSpan); } @@ -1631,7 +1645,7 @@ namespace Avalonia.Controls if (def.UserSize.IsStar) { - Debug.Assert(!def.IsShared, "*-defs cannot be shared"); + // Debug.Assert(!def.IsShared, "*-defs cannot be shared"); if (def.MeasureSize < 0.0) { @@ -1675,18 +1689,18 @@ namespace Avalonia.Controls 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; - } + // if (def.IsShared) + // { + // // overriding userMaxSize effectively prevents squishy-ness. + // // this is a "solution" to avoid shared definitions from been sized to + // // different final size at arrange time, if / when different grids receive + // // different final sizes. + // userMaxSize = userSize; + // } + // else + // { + userMaxSize = def.UserMaxSize; + // } def.SizeCache = Math.Max(def.MinSize, Math.Min(userSize, userMaxSize)); takenSize += def.SizeCache; @@ -2107,7 +2121,7 @@ namespace Avalonia.Controls if (_tempDefinitions != null) { // TempDefinitions has to be cleared to avoid "memory leaks" - Array.Clear(_tempDefinitions, 0, Math.Max(DefinitionsU.Length, DefinitionsV.Length)); + Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); _tempDefinitions = null; } } @@ -2232,7 +2246,7 @@ namespace Avalonia.Controls { get { - int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length) * 2; + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; if (_tempDefinitions == null || _tempDefinitions.Length < requiredLength) @@ -2265,7 +2279,7 @@ namespace Avalonia.Controls { get { - int requiredLength = Math.Max(Math.Max(DefinitionsU.Length, DefinitionsV.Length), 1) * 2; + int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; if (_definitionIndices == null || _definitionIndices.Length < requiredLength) { @@ -2283,7 +2297,7 @@ namespace Avalonia.Controls { get { - int requiredLength = Math.Max(DefinitionsU.Length, DefinitionsV.Length); + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); if (_roundingErrors == null && requiredLength == 0) { From 49bcea0e33ec975763c5399700b343ed4461371e Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 18:01:31 +0800 Subject: [PATCH 052/179] Disable SharedSize tests for now. --- .../Avalonia.Controls.UnitTests/GridTests.cs | 1 - .../SharedSizeScopeTests.cs | 88 +++++++++---------- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 5799cb91c4..50335c44ee 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -179,6 +179,5 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } - } } diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs index 715e9da880..c49d587a08 100644 --- a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -17,50 +17,50 @@ namespace Avalonia.Controls.UnitTests { } - [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 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() From 973e82386efb6e10f165d399e3cf928a292e0faf Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 19:10:38 +0800 Subject: [PATCH 053/179] Disable Shared Size Tests pt. 2 --- .../SharedSizeScopeTests.cs | 378 +++++++++--------- 1 file changed, 189 insertions(+), 189 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs index c49d587a08..03eceb17f5 100644 --- a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -62,223 +62,223 @@ namespace Avalonia.Controls.UnitTests // 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); + // [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; + // 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); - } + // 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_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); - [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); - } + // var outerGrid = CreateGrid(("A", new GridLength(0))); + // var outerScope = new Panel(); + // outerScope.Children.AddRange(new[] { outerGrid, innerScope }); - [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 = outerScope; - 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, outerGrid.ColumnDefinitions[0].ActualWidth); + // } - root.Measure(new Size(50, 50)); - root.Arrange(new Rect(new Point(), new Point(50, 50))); - Assert.Equal(0, grids[0].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") + // }; - grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; + // grid.ColumnDefinitions[1].SharedSizeGroup = "A"; + // grid.RowDefinitions[1].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); + // var root = new TestRoot(); + // root.SetValue(Grid.IsSharedSizeScopeProperty, true); + // root.Child = grid; - grids[0].ColumnDefinitions[0].SharedSizeGroup = null; + // root.Measure(new Size(50, 50)); + // root.Arrange(new Rect(new Point(), new Point(50, 50))); + // Assert.Equal(30, grid.RowDefinitions[1].ActualHeight); + // } - 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 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); - [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 root = new TestRoot(); + // root.SetValue(Grid.IsSharedSizeScopeProperty, true); + // root.Child = scope; - var scope = new Panel(); - scope.Children.Add(grid); + // root.Measure(new Size(50, 50)); + // root.Arrange(new Rect(new Point(), new Point(50, 50))); + // Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); - var root = new TestRoot(); - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - root.Child = scope; + // grids[0].ColumnDefinitions[0].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(40, cd.ActualWidth)); + // root.Measure(new Size(51, 51)); + // root.Arrange(new Rect(new Point(), new Point(51, 51))); + // Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); - grid.ColumnDefinitions.RemoveAt(2); + // grids[0].ColumnDefinitions[0].SharedSizeGroup = null; - 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)); + // root.Measure(new Size(52, 52)); + // root.Arrange(new Rect(new Point(), new Point(52, 52))); + // Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + // } - grid.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" }); + // [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())); - 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)); + // var scope = new Panel(); + // scope.Children.Add(grid); - grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; + // 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(30, cd.ActualWidth)); + // 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[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; + // 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(50, cd.ActualWidth)); - } + // 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)); - [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.ColumnDefinitions.Insert(1, new ColumnDefinition { Width = new GridLength(35), SharedSizeGroup = "A" }); - // grid creators - private Grid CreateGrid(params string[] columnGroups) - { - return CreateGrid(columnGroups.Select(s => (s, ColumnDefinition.WidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } + // 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)); - private Grid CreateGrid(params (string name, GridLength width)[] columns) - { - return CreateGrid(columns.Select(c => - (c.name, c.width, ColumnDefinition.MinWidthProperty.GetDefaultValue(typeof(ColumnDefinition)))).ToArray()); - } + // grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(10), SharedSizeGroup = "A" }; - 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()); - } + // 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)); - 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; - } + // grid.ColumnDefinitions[1] = new ColumnDefinition { Width = new GridLength(50), SharedSizeGroup = "A" }; - 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; - } + // 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; + // } } } From 52c562bffd2f67b9e79b39a9d9285c0c2bb8693b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 19:17:23 +0800 Subject: [PATCH 054/179] Remove old Grid classes. --- .../Avalonia.Controls.csproj | 1 - src/Avalonia.Controls/Grid.cs | 2886 +++++++++++++++-- src/Avalonia.Controls/GridWPF.cs | 2824 ---------------- src/Avalonia.Controls/Utils/GridLayout.cs | 705 ---- .../Utils/SharedSizeScopeHost.cs | 651 ---- .../GridLayoutTests.cs | 184 -- 6 files changed, 2555 insertions(+), 4696 deletions(-) delete mode 100644 src/Avalonia.Controls/GridWPF.cs delete mode 100644 src/Avalonia.Controls/Utils/GridLayout.cs delete mode 100644 src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index c321d3a2f1..32331d29ab 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index ad5e96376d..236b478f95 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -1,5 +1,5 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; @@ -10,15 +10,81 @@ using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; using Avalonia.VisualTree; +using System.Threading; using JetBrains.Annotations; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia; +using System.Collections; +using Avalonia.Utilities; +using Avalonia.Layout; namespace Avalonia.Controls { /// - /// Lays out child controls according to a grid. + /// Grid /// public class Grid : Panel { + internal bool CellsStructureDirty = true; + internal bool SizeToContentU; + internal bool SizeToContentV; + internal bool HasStarCellsU; + internal bool HasStarCellsV; + internal bool HasGroup3CellsInAutoRows; + internal bool ColumnDefinitionsDirty = true; + internal bool RowDefinitionsDirty = true; + + // index of the first cell in first cell group + internal int CellGroup1; + + // index of the first cell in second cell group + internal int CellGroup2; + + // index of the first cell in third cell group + internal int CellGroup3; + + // index of the first cell in fourth cell group + internal int CellGroup4; + + // temporary array used during layout for various purposes + // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) + private DefinitionBase[] _tempDefinitions; + private GridLinesRenderer _gridLinesRenderer; + + // Keeps track of definition indices. + private int[] _definitionIndices; + + private CellCache[] _cellCache; + + + // Stores unrounded values and rounding errors during layout rounding. + private double[] _roundingErrors; + private DefinitionBase[] _definitionsU; + private DefinitionBase[] _definitionsV; + + // 5 is an arbitrary constant chosen to end the measure loop + private const int _layoutLoopMaxCount = 5; + private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot; + private static readonly IComparer _spanPreferredDistributionOrderComparer; + private static readonly IComparer _spanMaxDistributionOrderComparer; + private static readonly IComparer _minRatioComparer; + private static readonly IComparer _maxRatioComparer; + private static readonly IComparer _starWeightComparer; + + static Grid() + { + ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + + _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); + _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); + _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); + _minRatioComparer = new MinRatioComparer(); + _maxRatioComparer = new MaxRatioComparer(); + _starWeightComparer = new StarWeightComparer(); + } + /// /// Defines the Column attached property. /// @@ -50,91 +116,21 @@ namespace Avalonia.Controls public static readonly AttachedProperty IsSharedSizeScopeProperty = AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - protected override void OnMeasureInvalidated() - { - base.OnMeasureInvalidated(); - _sharedSizeHost?.InvalidateMeasure(this); - } - - private SharedSizeScopeHost _sharedSizeHost; - - /// - /// Defines the SharedSizeScopeHost private property. - /// The ampersands are used to make accessing the property via xaml inconvenient. - /// - internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); - - private ColumnDefinitions _columnDefinitions; - - private RowDefinitions _rowDefinitions; - - static Grid() - { - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); - } - - public Grid() - { - this.AttachedToVisualTree += Grid_AttachedToVisualTree; - this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; - } - /// - /// Gets or sets the columns definitions for the grid. + /// Defines the property. /// - public ColumnDefinitions ColumnDefinitions - { - get - { - if (_columnDefinitions == null) - { - ColumnDefinitions = new ColumnDefinitions(); - } - - return _columnDefinitions; - } - - set - { - if (_columnDefinitions != null) - { - throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); - } - - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _columnDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); - } - } + public static readonly StyledProperty ShowGridLinesProperty = + AvaloniaProperty.Register( + nameof(ShowGridLines), + defaultValue: true); /// - /// Gets or sets the row definitions for the grid. + /// ShowGridLines property. /// - public RowDefinitions RowDefinitions + public bool ShowGridLines { - get - { - if (_rowDefinitions == null) - { - RowDefinitions = new RowDefinitions(); - } - - return _rowDefinitions; - } - - set - { - if (_rowDefinitions != null) - { - throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); - } - - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => InvalidateMeasure()); - _rowDefinitions.CollectionChanged += (_, __) => InvalidateMeasure(); - } + get { return GetValue(ShowGridLinesProperty); } + set { SetValue(ShowGridLinesProperty, value); } } /// @@ -177,7 +173,6 @@ namespace Avalonia.Controls return element.GetValue(RowSpanProperty); } - /// /// Gets the value of the IsSharedSizeScope attached property for a control. /// @@ -228,373 +223,2602 @@ namespace Avalonia.Controls element.SetValue(RowSpanProperty, value); } + private ColumnDefinitions _columnDefinitions; + private RowDefinitions _rowDefinitions; + /// - /// Sets the value of IsSharedSizeScope property for a control. + /// Gets or sets the columns definitions for the grid. /// - /// The control. - /// The IsSharedSizeScope value. - public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) + public ColumnDefinitions ColumnDefinitions { - element.SetValue(IsSharedSizeScopeProperty, value); + get + { + if (_columnDefinitions == null) + { + ColumnDefinitions = new ColumnDefinitions(); + } + + return _columnDefinitions; + } + set + { + _columnDefinitions = value; + _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); + ColumnDefinitionsDirty = true; + + if (_columnDefinitions.Count > 0) + _definitionsU = _columnDefinitions.Cast().ToArray(); + else + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + + _columnDefinitions.CollectionChanged += (_, e) => + { + if (_columnDefinitions.Count == 0) + { + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + } + else + { + _definitionsU = _columnDefinitions.Cast().ToArray(); + ColumnDefinitionsDirty = true; + } + Invalidate(); + }; + } } /// - /// Gets the result of the last column measurement. - /// Use this result to reduce the arrange calculation. + /// Gets or sets the row definitions for the grid. /// - private GridLayout.MeasureResult _columnMeasureCache; + public RowDefinitions RowDefinitions + { + get + { + if (_rowDefinitions == null) + { + RowDefinitions = new RowDefinitions(); + } - /// - /// Gets the result of the last row measurement. - /// Use this result to reduce the arrange calculation. - /// - private GridLayout.MeasureResult _rowMeasureCache; + return _rowDefinitions; + } + set + { + _rowDefinitions = value; + _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - /// - /// Gets the row layout as of the last measure. - /// - private GridLayout _rowLayoutCache; + RowDefinitionsDirty = true; - /// - /// Gets the column layout as of the last measure. - /// - private GridLayout _columnLayoutCache; + if (_rowDefinitions.Count > 0) + _definitionsV = _rowDefinitions.Cast().ToArray(); + else + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; + + _rowDefinitions.CollectionChanged += (_, e) => + { + if (_rowDefinitions.Count == 0) + { + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; + } + else + { + _definitionsV = _rowDefinitions.Cast().ToArray(); + RowDefinitionsDirty = true; + } + Invalidate(); + }; + } + } + + private bool IsTrivialGrid => (_definitionsU?.Length <= 1) && + (_definitionsV?.Length <= 1); /// - /// Measures the grid. + /// Content measurement. /// - /// The available size. - /// The desired size of the control. + /// Constraint + /// Desired size protected override Size MeasureOverride(Size constraint) { - // Situation 1/2: - // If the grid doesn't have any column/row definitions, it behaves like a normal panel. - // GridLayout supports this situation but we handle this separately for performance. + Size gridDesiredSize; - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + try { - var maxWidth = 0.0; - var maxHeight = 0.0; - foreach (var child in Children.OfType()) + if (IsTrivialGrid) { - child.Measure(constraint); - maxWidth = Math.Max(maxWidth, child.DesiredSize.Width); - maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); + gridDesiredSize = new Size(); + + for (int i = 0, count = Children.Count; i < count; ++i) + { + var child = Children[i]; + if (child != null) + { + child.Measure(constraint); + gridDesiredSize = new Size( + Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); + } + } + } + else + { + { + bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); + bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); + + // Clear index information and rounding errors + if (RowDefinitionsDirty || ColumnDefinitionsDirty) + { + if (_definitionIndices != null) + { + Array.Clear(_definitionIndices, 0, _definitionIndices.Length); + _definitionIndices = null; + } + + if (UseLayoutRounding) + { + if (_roundingErrors != null) + { + Array.Clear(_roundingErrors, 0, _roundingErrors.Length); + _roundingErrors = null; + } + } + } + + ValidateColumnDefinitionsStructure(); + ValidateDefinitionsLayout(_definitionsU, sizeToContentU); + + ValidateRowDefinitionsStructure(); + ValidateDefinitionsLayout(_definitionsV, sizeToContentV); + + CellsStructureDirty |= (SizeToContentU != sizeToContentU) + || (SizeToContentV != sizeToContentV); + + SizeToContentU = sizeToContentU; + SizeToContentV = sizeToContentV; + } + + ValidateCells(); + + Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); + + MeasureCellsGroup(CellGroup1, constraint, false, false); + + { + // after Group1 is measured, only Group3 may have cells belonging to Auto rows. + bool canResolveStarsV = !HasGroup3CellsInAutoRows; + + if (canResolveStarsV) + { + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + MeasureCellsGroup(CellGroup2, constraint, false, false); + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } + MeasureCellsGroup(CellGroup3, constraint, false, false); + } + else + { + // if at least one cell exists in Group2, it must be measured before + // StarsU can be resolved. + bool canResolveStarsU = CellGroup2 > _cellCache.Length; + if (canResolveStarsU) + { + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } + MeasureCellsGroup(CellGroup3, constraint, false, false); + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + } + else + { + // This is a revision to the algorithm employed for the cyclic + // dependency case described above. We now repeatedly + // measure Group3 and Group2 until their sizes settle. We + // also use a count heuristic to break a loop in case of one. + + bool hasDesiredSizeUChanged = false; + int cnt = 0; + + // Cache Group2MinWidths & Group3MinHeights + double[] group2MinSizes = CacheMinSizes(CellGroup2, false); + double[] group3MinSizes = CacheMinSizes(CellGroup3, true); + + MeasureCellsGroup(CellGroup2, constraint, false, true); + + do + { + if (hasDesiredSizeUChanged) + { + // Reset cached Group3Heights + ApplyCachedMinSizes(group3MinSizes, true); + } + + if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } + MeasureCellsGroup(CellGroup3, constraint, false, false); + + // Reset cached Group2Widths + ApplyCachedMinSizes(group2MinSizes, false); + + if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } + MeasureCellsGroup(CellGroup2, constraint, + cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + } + while (hasDesiredSizeUChanged && ++cnt <= _layoutLoopMaxCount); + } + } + } + + MeasureCellsGroup(CellGroup4, constraint, false, false); + + gridDesiredSize = new Size( + CalculateDesiredSize(_definitionsU), + CalculateDesiredSize(_definitionsV)); } - - maxWidth = Math.Min(maxWidth, constraint.Width); - maxHeight = Math.Min(maxHeight, constraint.Height); - return new Size(maxWidth, maxHeight); } - - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. - - var measureCache = new Dictionary(); - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - // Note: If a child stays in a * or Auto column/row, use constraint to measure it. - columnLayout.AppendMeasureConventions(safeColumns, child => MeasureOnce(child, constraint).Width); - rowLayout.AppendMeasureConventions(safeRows, child => MeasureOnce(child, constraint).Height); - - // Calculate measurement. - var columnResult = columnLayout.Measure(constraint.Width); - var rowResult = rowLayout.Measure(constraint.Height); - - // Use the results of the measurement to measure the rest of the children. - foreach (var child in Children.OfType()) + finally { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var width = Enumerable.Range(column, columnSpan).Select(x => columnResult.LengthList[x]).Sum(); - var height = Enumerable.Range(row, rowSpan).Select(x => rowResult.LengthList[x]).Sum(); - - MeasureOnce(child, new Size(width, height)); } - // Cache the measure result and return the desired size. - _columnMeasureCache = columnResult; - _rowMeasureCache = rowResult; - _rowLayoutCache = rowLayout; - _columnLayoutCache = columnLayout; + return (gridDesiredSize); + } - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) + private void ValidateColumnDefinitionsStructure() + { + if (ColumnDefinitionsDirty) { - _sharedSizeHost.UpdateMeasureStatus(this, rowResult, columnResult); + if (_definitionsU == null) + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + ColumnDefinitionsDirty = false; } + } - return new Size(columnResult.DesiredLength, rowResult.DesiredLength); - - // Measure each child only once. - // If a child has been measured, it will just return the desired size. - Size MeasureOnce(Control child, Size size) + private void ValidateRowDefinitionsStructure() + { + if (RowDefinitionsDirty) { - if (measureCache.TryGetValue(child, out var desiredSize)) - { - return desiredSize; - } + if (_definitionsV == null) + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - child.Measure(size); - desiredSize = child.DesiredSize; - measureCache[child] = desiredSize; - return desiredSize; + RowDefinitionsDirty = false; } } /// - /// Arranges the grid's children. + /// Content arrangement. /// - /// The size allocated to the control. - /// The space taken. - protected override Size ArrangeOverride(Size finalSize) + /// Arrange size + protected override Size ArrangeOverride(Size arrangeSize) { - // Situation 1/2: - // If the grid doesn't have any column/row definitions, it behaves like a normal panel. - // GridLayout supports this situation but we handle this separately for performance. - - if (ColumnDefinitions.Count == 0 && RowDefinitions.Count == 0) + try { - foreach (var child in Children.OfType()) + if (IsTrivialGrid) { - child.Arrange(new Rect(finalSize)); + for (int i = 0, count = Children.Count; i < count; ++i) + { + var child = Children[i]; + if (child != null) + { + child.Arrange(new Rect(arrangeSize)); + } + } + } + else + { + Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); + + SetFinalSize(_definitionsU, arrangeSize.Width, true); + SetFinalSize(_definitionsV, arrangeSize.Height, false); + + for (int currentCell = 0; currentCell < _cellCache.Length; ++currentCell) + { + IControl cell = Children[currentCell]; + if (cell == null) + { + continue; + } + + int columnIndex = _cellCache[currentCell].ColumnIndex; + int rowIndex = _cellCache[currentCell].RowIndex; + int columnSpan = _cellCache[currentCell].ColumnSpan; + int rowSpan = _cellCache[currentCell].RowSpan; + + Rect cellRect = new Rect( + columnIndex == 0 ? 0.0 : _definitionsU[columnIndex].FinalOffset, + rowIndex == 0 ? 0.0 : _definitionsV[rowIndex].FinalOffset, + GetFinalSizeForRange(_definitionsU, columnIndex, columnSpan), + GetFinalSizeForRange(_definitionsV, rowIndex, rowSpan)); + + cell.Arrange(cellRect); + } + + // update render bound on grid lines renderer visual + var gridLinesRenderer = EnsureGridLinesRenderer(); + if (gridLinesRenderer != null) + { + gridLinesRenderer.UpdateRenderBounds(arrangeSize); + } } - - return finalSize; - } - - // Situation 2/2: - // If the grid defines some columns or rows. - // Debug Tip: - // - GridLayout doesn't hold any state, so you can drag the debugger execution - // arrow back to any statements and re-run them without any side-effect. - - var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = _columnLayoutCache; - var rowLayout = _rowLayoutCache; - - var rowCache = _rowMeasureCache; - var columnCache = _columnMeasureCache; - - if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { - (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); - - rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); - columnCache = columnLayout.Measure(finalSize.Width, columnCache.LeanLengthList); } - - // Calculate for arrange result. - var columnResult = columnLayout.Arrange(finalSize.Width, columnCache); - var rowResult = rowLayout.Arrange(finalSize.Height, rowCache); - // Arrange the children. - foreach (var child in Children.OfType()) + finally { - var (column, columnSpan) = safeColumns[child]; - var (row, rowSpan) = safeRows[child]; - var x = Enumerable.Range(0, column).Sum(c => columnResult.LengthList[c]); - var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); - var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); - var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); - child.Arrange(new Rect(x, y, width, height)); + SetValid(); } - // Assign the actual width. for (var i = 0; i < ColumnDefinitions.Count; i++) { - ColumnDefinitions[i].ActualWidth = columnResult.LengthList[i]; + ColumnDefinitions[i].ActualWidth = GetFinalColumnDefinitionWidth(i); } - // Assign the actual height. for (var i = 0; i < RowDefinitions.Count; i++) { - RowDefinitions[i].ActualHeight = rowResult.LengthList[i]; + RowDefinitions[i].ActualHeight = GetFinalRowDefinitionHeight(i); } - // Return the render size. - return finalSize; + return (arrangeSize); } /// - /// Tests whether this grid belongs to a shared size scope. + /// Returns final width for a column. /// - /// True if the grid is registered in a shared size scope. - internal bool HasSharedSizeScope() + /// + /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. + /// + internal double GetFinalColumnDefinitionWidth(int columnIndex) { - return _sharedSizeHost != null; + double value = 0.0; + + // actual value calculations require structure to be up-to-date + if (!ColumnDefinitionsDirty) + { + value = _definitionsU[(columnIndex + 1) % _definitionsU.Length].FinalOffset; + if (columnIndex != 0) { value -= _definitionsU[columnIndex].FinalOffset; } + } + return (value); } /// - /// Called when the SharedSizeScope for a given grid has changed. - /// Unregisters the grid from it's current scope and finds a new one (if any) + /// Returns final height for a row. /// /// - /// This method, while not efficient, correctly handles nested scopes, with any order of scope changes. + /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. /// - internal void SharedScopeChanged() + internal double GetFinalRowDefinitionHeight(int rowIndex) { - _sharedSizeHost?.UnegisterGrid(this); - - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + double value = 0.0; - if (scope != null) + // actual value calculations require structure to be up-to-date + if (!RowDefinitionsDirty) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + value = _definitionsV[(rowIndex + 1) % _definitionsV.Length].FinalOffset; + if (rowIndex != 0) { value -= _definitionsV[rowIndex].FinalOffset; } } + return (value); + } + /// + /// Invalidates grid caches and makes the grid dirty for measure. + /// + internal void Invalidate() + { + CellsStructureDirty = true; InvalidateMeasure(); } /// - /// Callback when a grid is attached to the visual tree. Finds the innermost SharedSizeScope and registers the grid - /// in it. + /// Lays out cells according to rows and columns, and creates lookup grids. /// - /// The source of the event. - /// The event arguments. - private void Grid_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e) + private void ValidateCells() { - var scope = - new Control[] { this }.Concat(this.GetVisualAncestors().OfType()) - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + if (!CellsStructureDirty) return; - if (_sharedSizeHost != null) - throw new AvaloniaInternalException("Shared size scope already present when attaching to visual tree!"); + _cellCache = new CellCache[Children.Count]; + CellGroup1 = int.MaxValue; + CellGroup2 = int.MaxValue; + CellGroup3 = int.MaxValue; + CellGroup4 = int.MaxValue; - if (scope != null) + bool hasStarCellsU = false; + bool hasStarCellsV = false; + bool hasGroup3CellsInAutoRows = false; + + for (int i = _cellCache.Length - 1; i >= 0; --i) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + var child = Children[i] as Control; + + if (child == null) + { + continue; + } + + var cell = new CellCache(); + + // read indices from the corresponding properties + // clamp to value < number_of_columns + // column >= 0 is guaranteed by property value validation callback + cell.ColumnIndex = Math.Min(GetColumn(child), _definitionsU.Length - 1); + + // clamp to value < number_of_rows + // row >= 0 is guaranteed by property value validation callback + cell.RowIndex = Math.Min(GetRow(child), _definitionsV.Length - 1); + + // read span properties + // clamp to not exceed beyond right side of the grid + // column_span > 0 is guaranteed by property value validation callback + cell.ColumnSpan = Math.Min(GetColumnSpan(child), _definitionsU.Length - cell.ColumnIndex); + + // clamp to not exceed beyond bottom side of the grid + // row_span > 0 is guaranteed by property value validation callback + cell.RowSpan = Math.Min(GetRowSpan(child), _definitionsV.Length - cell.RowIndex); + + Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < _definitionsU.Length); + Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < _definitionsV.Length); + + // + // calculate and cache length types for the child + // + cell.SizeTypeU = GetLengthTypeForRange(_definitionsU, cell.ColumnIndex, cell.ColumnSpan); + cell.SizeTypeV = GetLengthTypeForRange(_definitionsV, cell.RowIndex, cell.RowSpan); + + hasStarCellsU |= cell.IsStarU; + hasStarCellsV |= cell.IsStarV; + + // + // distribute cells into four groups. + // + if (!cell.IsStarV) + { + if (!cell.IsStarU) + { + cell.Next = CellGroup1; + CellGroup1 = i; + } + else + { + cell.Next = CellGroup3; + CellGroup3 = i; + + // remember if this cell belongs to auto row + hasGroup3CellsInAutoRows |= cell.IsAutoV; + } + } + else + { + if (cell.IsAutoU + // note below: if spans through Star column it is NOT Auto + && !cell.IsStarU) + { + cell.Next = CellGroup2; + CellGroup2 = i; + } + else + { + cell.Next = CellGroup4; + CellGroup4 = i; + } + } + + _cellCache[i] = cell; } + + HasStarCellsU = hasStarCellsU; + HasStarCellsV = hasStarCellsV; + HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; + + CellsStructureDirty = false; } /// - /// Callback when a grid is detached from the visual tree. Unregisters the grid from its SharedSizeScope if any. + /// Validates layout time size type information on given array of definitions. + /// Sets MinSize and MeasureSizes. /// - /// The source of the event. - /// The event arguments. - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) + /// Array of definitions to update. + /// if "true" then star definitions are treated as Auto. + private void ValidateDefinitionsLayout( + DefinitionBase[] definitions, + bool treatStarAsAuto) { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } + for (int i = 0; i < definitions.Length; ++i) + { + // Reset minimum size. + definitions[i].MinSize = 0; + double userMinSize = definitions[i].UserMinSize; + double userMaxSize = definitions[i].UserMaxSize; + double userSize = 0; - /// - /// Get the safe column/columnspan and safe row/rowspan. - /// This method ensures that none of the children has a column/row outside the bounds of the definitions. - /// - [Pure] - private (Dictionary safeColumns, - Dictionary safeRows) GetSafeColumnRows() - { - var columnCount = ColumnDefinitions.Count; - var rowCount = RowDefinitions.Count; - columnCount = columnCount == 0 ? 1 : columnCount; - rowCount = rowCount == 0 ? 1 : rowCount; - var safeColumns = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(columnCount, GetColumn(child), GetColumnSpan(child))); - var safeRows = Children.OfType().ToDictionary(child => child, - child => GetSafeSpan(rowCount, GetRow(child), GetRowSpan(child))); - return (safeColumns, safeRows); + switch (definitions[i].UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + definitions[i].SizeType = LayoutTimeSizeType.Pixel; + userSize = definitions[i].UserSize.Value; + + // this was brought with NewLayout and defeats squishy behavior + userMinSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + break; + case (GridUnitType.Auto): + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + break; + case (GridUnitType.Star): + if (treatStarAsAuto) + { + definitions[i].SizeType = LayoutTimeSizeType.Auto; + userSize = double.PositiveInfinity; + } + else + { + definitions[i].SizeType = LayoutTimeSizeType.Star; + userSize = double.PositiveInfinity; + } + break; + default: + Debug.Assert(false); + break; + } + + definitions[i].UpdateMinSize(userMinSize); + definitions[i].MeasureSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); + } } - /// - /// Gets the safe row/column and rowspan/columnspan for a specified range. - /// The user may assign row/column properties outside the bounds of the row/column count, this method coerces them inside. - /// - /// The row or column count. - /// The row or column that the user assigned. - /// The rowspan or columnspan that the user assigned. - /// The safe row/column and rowspan/columnspan. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (int index, int span) GetSafeSpan(int length, int userIndex, int userSpan) + private double[] CacheMinSizes(int cellsHead, bool isRows) { - var index = userIndex; - var span = userSpan; + double[] minSizes = isRows ? new double[_definitionsV.Length] + : new double[_definitionsU.Length]; - if (index < 0) + for (int j = 0; j < minSizes.Length; j++) { - span = index + span; - index = 0; + minSizes[j] = -1; } - if (span <= 0) + int i = cellsHead; + do { - span = 1; - } + if (isRows) + { + minSizes[_cellCache[i].RowIndex] = _definitionsV[_cellCache[i].RowIndex].MinSize; + } + else + { + minSizes[_cellCache[i].ColumnIndex] = _definitionsU[_cellCache[i].ColumnIndex].MinSize; + } - if (userIndex >= length) - { - index = length - 1; - span = 1; - } - else if (userIndex + userSpan > length) + i = _cellCache[i].Next; + } while (i < _cellCache.Length); + + return minSizes; + } + + private void ApplyCachedMinSizes(double[] minSizes, bool isRows) + { + for (int i = 0; i < minSizes.Length; i++) { - span = length - userIndex; + if (MathUtilities.GreaterThanOrClose(minSizes[i], 0)) + { + if (isRows) + { + _definitionsV[i].MinSize = minSizes[i]; + } + else + { + _definitionsU[i].MinSize = minSizes[i]; + } + } } + } - return (index, span); + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV) + { + bool unusedHasDesiredSizeUChanged; + MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, + forceInfinityV, out unusedHasDesiredSizeUChanged); } - private static int ValidateColumn(AvaloniaObject o, int value) + /// + /// Measures one group of cells. + /// + /// Head index of the cells chain. + /// Reference size for spanned cells + /// calculations. + /// When "true" cells' desired + /// width is not registered in columns. + /// Passed through to MeasureCell. + /// When "true" cells' desired height is not registered in rows. + private void MeasureCellsGroup( + int cellsHead, + Size referenceSize, + bool ignoreDesiredSizeU, + bool forceInfinityV, + out bool hasDesiredSizeUChanged) { - if (value < 0) + hasDesiredSizeUChanged = false; + + if (cellsHead >= _cellCache.Length) { - throw new ArgumentException("Invalid Grid.Column value."); + return; } - return value; + Hashtable spanStore = null; + bool ignoreDesiredSizeV = forceInfinityV; + + int i = cellsHead; + do + { + double oldWidth = Children[i].DesiredSize.Width; + + MeasureCell(i, forceInfinityV); + + hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, Children[i].DesiredSize.Width); + + if (!ignoreDesiredSizeU) + { + if (_cellCache[i].ColumnSpan == 1) + { + _definitionsU[_cellCache[i].ColumnIndex] + .UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, + _definitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + _cellCache[i].ColumnIndex, + _cellCache[i].ColumnSpan, + true, + Children[i].DesiredSize.Width); + } + } + + if (!ignoreDesiredSizeV) + { + if (_cellCache[i].RowSpan == 1) + { + _definitionsV[_cellCache[i].RowIndex] + .UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, + _definitionsV[_cellCache[i].RowIndex].UserMaxSize)); + } + else + { + RegisterSpan( + ref spanStore, + _cellCache[i].RowIndex, + _cellCache[i].RowSpan, + false, + Children[i].DesiredSize.Height); + } + } + + i = _cellCache[i].Next; + } while (i < _cellCache.Length); + + if (spanStore != null) + { + foreach (DictionaryEntry e in spanStore) + { + SpanKey key = (SpanKey)e.Key; + double requestedSize = (double)e.Value; + + EnsureMinSizeInDefinitionRange( + key.U ? _definitionsU : _definitionsV, + key.Start, + key.Count, + requestedSize, + key.U ? referenceSize.Width : referenceSize.Height); + } + } } - private static int ValidateRow(AvaloniaObject o, int value) + /// + /// Helper method to register a span information for delayed processing. + /// + /// Reference to a hashtable object used as storage. + /// Span starting index. + /// Span count. + /// true if this is a column span. false if this is a row span. + /// Value to store. If an entry already exists the biggest value is stored. + private static void RegisterSpan( + ref Hashtable store, + int start, + int count, + bool u, + double value) { - if (value < 0) + if (store == null) { - throw new ArgumentException("Invalid Grid.Row value."); + store = new Hashtable(); } - return value; + SpanKey key = new SpanKey(start, count, u); + object o = store[key]; + + if (o == null + || value > (double)o) + { + store[key] = value; + } } /// - /// Called when the value of changes for a control. + /// Takes care of measuring a single cell. /// - /// The control that triggered the change. - /// Change arguments. - private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + /// Index of the cell to measure. + /// If "true" then cell is always + /// calculated to infinite height. + private void MeasureCell( + int cell, + bool forceInfinityV) { - var shouldDispose = (arg2.OldValue is bool d) && d; - if (shouldDispose) + double cellMeasureWidth; + double cellMeasureHeight; + + if (_cellCache[cell].IsAutoU + && !_cellCache[cell].IsStarU) { - var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; - if (host == null) - throw new AvaloniaInternalException("SharedScopeHost wasn't set when IsSharedSizeScope was true!"); - host.Dispose(); - source.ClearValue(s_sharedSizeScopeHostProperty); + // if cell belongs to at least one Auto column and not a single Star column + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureWidth = double.PositiveInfinity; } - - var shouldAssign = (arg2.NewValue is bool a) && a; - if (shouldAssign) + else { - if (source.GetValue(s_sharedSizeScopeHostProperty) != null) - throw new AvaloniaInternalException("SharedScopeHost was already set when IsSharedSizeScope is only now being set to true!"); - source.SetValue(s_sharedSizeScopeHostProperty, new SharedSizeScopeHost()); + // otherwise... + cellMeasureWidth = GetMeasureSizeForRange( + _definitionsU, + _cellCache[cell].ColumnIndex, + _cellCache[cell].ColumnSpan); } - // if the scope has changed, notify the descendant grids that they need to update. - if (source.GetVisualRoot() != null && shouldAssign || shouldDispose) + if (forceInfinityV) { - var participatingGrids = new[] { source }.Concat(source.GetVisualDescendants()).OfType(); + cellMeasureHeight = double.PositiveInfinity; + } + else if (_cellCache[cell].IsAutoV + && !_cellCache[cell].IsStarV) + { + // if cell belongs to at least one Auto row and not a single Star row + // then it should be calculated "to content", thus it is possible to "shortcut" + // calculations and simply assign PositiveInfinity here. + cellMeasureHeight = double.PositiveInfinity; + } + else + { + cellMeasureHeight = GetMeasureSizeForRange( + _definitionsV, + _cellCache[cell].RowIndex, + _cellCache[cell].RowSpan); + } + + var child = Children[cell]; - foreach (var grid in participatingGrids) - grid.SharedScopeChanged(); + if (child != null) + { + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); } } + + /// + /// Calculates one dimensional measure size for given definitions' range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Calculated measure size. + /// + /// For "Auto" definitions MinWidth is used in place of PreferredSize. + /// + private double GetMeasureSizeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); + + double measureSize = 0; + int i = start + count - 1; + + do + { + measureSize += (definitions[i].SizeType == LayoutTimeSizeType.Auto) + ? definitions[i].MinSize + : definitions[i].MeasureSize; + } while (--i >= start); + + return (measureSize); + } + + /// + /// Accumulates length type information for given definition's range. + /// + /// Source array of definitions to read values from. + /// Starting index of the range. + /// Number of definitions included in the range. + /// Length type for given range. + private LayoutTimeSizeType GetLengthTypeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); + + LayoutTimeSizeType lengthType = LayoutTimeSizeType.None; + int i = start + count - 1; + + do + { + lengthType |= definitions[i].SizeType; + } while (--i >= start); + + return (lengthType); + } + + /// + /// Distributes min size back to definition array's range. + /// + /// Start of the range. + /// Number of items in the range. + /// Minimum size that should "fit" into the definitions range. + /// Definition array receiving distribution. + /// Size used to resolve percentages. + private void EnsureMinSizeInDefinitionRange( + DefinitionBase[] definitions, + int start, + int count, + double requestedSize, + double percentReferenceSize) + { + Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Length); + + // avoid processing when asked to distribute "0" + if (!MathUtilities.IsZero(requestedSize)) + { + DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting + int end = start + count; + int autoDefinitionsCount = 0; + double rangeMinSize = 0; + double rangePreferredSize = 0; + double rangeMaxSize = 0; + double maxMaxSize = 0; // maximum of maximum sizes + + // first accumulate the necessary information: + // a) sum up the sizes in the range; + // b) count the number of auto definitions in the range; + // c) initialize temp array + // d) cache the maximum size into SizeCache + // e) accumulate max of max sizes + for (int i = start; i < end; ++i) + { + double minSize = definitions[i].MinSize; + double preferredSize = definitions[i].PreferredSize; + double maxSize = Math.Max(definitions[i].UserMaxSize, minSize); + + rangeMinSize += minSize; + rangePreferredSize += preferredSize; + rangeMaxSize += maxSize; + + definitions[i].SizeCache = maxSize; + + // sanity check: no matter what, but min size must always be the smaller; + // max size must be the biggest; and preferred should be in between + Debug.Assert(minSize <= preferredSize + && preferredSize <= maxSize + && rangeMinSize <= rangePreferredSize + && rangePreferredSize <= rangeMaxSize); + + if (maxMaxSize < maxSize) maxMaxSize = maxSize; + if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; + tempDefinitions[i - start] = definitions[i]; + } + + // avoid processing if the range already big enough + if (requestedSize > rangeMinSize) + { + if (requestedSize <= rangePreferredSize) + { + // + // requestedSize fits into preferred size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions - they should continue to stay "tight"; + // * for all non-auto definitions distribute to equi-size min sizes, without exceeding preferred size. + // + // in order to achieve that, definitions are sorted in a way that all auto definitions + // are first, then definitions follow ascending order with PreferredSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, _spanPreferredDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + // adjust sizeToDistribute value by subtracting auto definition min size + sizeToDistribute -= (tempDefinitions[i].MinSize); + } + + for (; i < count; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); + if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } + sizeToDistribute -= newMinSize; + } + + // sanity check: requested size must all be distributed + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); + } + else if (requestedSize <= rangeMaxSize) + { + // + // requestedSize bigger than preferred size, but fit into max size of the range. + // distribute according to the following logic: + // * do not distribute into auto definitions, if possible - they should continue to stay "tight"; + // * for all non-auto definitions distribute to euqi-size min sizes, without exceeding max size. + // + // in order to achieve that, definitions are sorted in a way that all non-auto definitions + // are last, then definitions follow ascending order with MaxSize as the key of sorting. + // + double sizeToDistribute; + int i; + + Array.Sort(tempDefinitions, 0, count, _spanMaxDistributionOrderComparer); + for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) + { + // sanity check: no auto definitions allowed in this loop + Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].PreferredSize; + double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + for (; i < count; ++i) + { + // sanity check: only auto definitions allowed in this loop + Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + + double preferredSize = tempDefinitions[i].MinSize; + double newMinSize = preferredSize + sizeToDistribute / (count - i); + tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); + sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + } + + // sanity check: requested size must all be distributed + Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); + } + else + { + // + // requestedSize bigger than max size of the range. + // distribute according to the following logic: + // * for all definitions distribute to equi-size min sizes. + // + double equalSize = requestedSize / count; + + if (equalSize < maxMaxSize + && !MathUtilities.AreClose(equalSize, maxMaxSize)) + { + // equi-size is less than maximum of maxSizes. + // in this case distribute so that smaller definitions grow faster than + // bigger ones. + double totalRemainingSize = maxMaxSize * count - rangeMaxSize; + double sizeToDistribute = requestedSize - rangeMaxSize; + + // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers + Debug.Assert(!double.IsInfinity(totalRemainingSize) + && !double.IsNaN(totalRemainingSize) + && totalRemainingSize > 0 + && !double.IsInfinity(sizeToDistribute) + && !double.IsNaN(sizeToDistribute) + && sizeToDistribute > 0); + + for (int i = 0; i < count; ++i) + { + double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + } + } + else + { + // + // equi-size is greater or equal to maximum of max sizes. + // all definitions receive equalSize as their mim sizes. + // + for (int i = 0; i < count; ++i) + { + tempDefinitions[i].UpdateMinSize(equalSize); + } + } + } + } + } + } + + // new implementation as of 4.7. Several improvements: + // 1. Allocate to *-defs hitting their min or max constraints, before allocating + // to other *-defs. A def that hits its min uses more space than its + // proportional share, reducing the space available to everyone else. + // The legacy algorithm deducted this space only from defs processed + // after the min; the new algorithm deducts it proportionally from all + // defs. This avoids the "*-defs exceed available space" problem, + // and other related problems where *-defs don't receive proportional + // allocations even though no constraints are preventing it. + // 2. When multiple defs hit min or max, resolve the one with maximum + // discrepancy (defined below). This avoids discontinuities - small + // change in available space resulting in large change to one def's allocation. + // 3. Correct handling of large *-values, including Infinity. + + /// + /// Resolves Star's for given array of definitions. + /// + /// Array of definitions to resolve stars. + /// All available size. + /// + /// Must initialize LayoutSize for all Star entries in given array of definitions. + /// + private void ResolveStar( + DefinitionBase[] definitions, + double availableSize) + { + int defCount = definitions.Length; + DefinitionBase[] tempDefinitions = TempDefinitions; + int minCount = 0, maxCount = 0; + double takenSize = 0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.SizeType == LayoutTimeSizeType.Star) + { + ++starCount; + def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" + if (def.UserSize.Value > maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3 = true; runPhase2and3;) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of tempDefinitions, + // the "max" list in the second half. TempDefinitions has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + switch (def.SizeType) + { + case (LayoutTimeSizeType.Auto): + takenSize += definitions[i].MinSize; + break; + case (LayoutTimeSizeType.Pixel): + takenSize += def.MeasureSize; + break; + case (LayoutTimeSizeType.Star): + if (def.MeasureSize < 0.0) + { + takenSize += -def.MeasureSize; // already resolved + } + else + { + double starWeight = StarWeight(def, scale); + totalStarWeight += starWeight; + + if (def.MinSize > 0.0) + { + // store ratio w/min in MeasureSize (for now) + tempDefinitions[minCount++] = def; + def.MeasureSize = starWeight / def.MinSize; + } + + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + if (!double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + tempDefinitions[defCount + maxCount++] = def; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + break; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = availableSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + Array.Sort(tempDefinitions, 0, minCount, _minRatioComparer); + Array.Sort(tempDefinitions, defCount, maxCount, _maxRatioComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.SizeType == LayoutTimeSizeType.Star && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedDef = tempDefinitions[minCount - 1]; + resolvedSize = resolvedDef.MinSize; + --minCount; + } + else + { + resolvedDef = tempDefinitions[defCount + maxCount - 1]; + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = availableSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) + { + --minCount; + tempDefinitions[minCount] = null; + } + while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) + { + --maxCount; + tempDefinitions[defCount + maxCount] = null; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < availableSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > availableSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + DefinitionBase def = tempDefinitions[defCount + i]; + if (def != null) + { + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.SizeType == LayoutTimeSizeType.Star) + { + if (def.MeasureSize < 0.0) + { + // this def was resolved in phase 3 - fix up its measure size + def.MeasureSize = -def.MeasureSize; + } + else + { + // this def needs resolution, add it to the list, sorted by *-weight + tempDefinitions[starCount++] = def; + def.MeasureSize = StarWeight(def, scale); + } + } + } + + if (starCount > 0) + { + Array.Sort(tempDefinitions, 0, starCount, _starWeightComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = tempDefinitions[i]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = tempDefinitions[i]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSize, resolvedSize); + + def.MeasureSize = resolvedSize; + takenSize += resolvedSize; + } + } + } + + /// + /// Calculates desired size for given array of definitions. + /// + /// Array of definitions to use for calculations. + /// Desired size. + private double CalculateDesiredSize( + DefinitionBase[] definitions) + { + double desiredSize = 0; + + for (int i = 0; i < definitions.Length; ++i) + { + desiredSize += definitions[i].MinSize; + } + + return (desiredSize); + } + + /// + /// Calculates and sets final size for all definitions in the given array. + /// + /// Array of definitions to process. + /// Final size to lay out to. + /// True if sizing row definitions, false for columns + private void SetFinalSize( + DefinitionBase[] definitions, + double finalSize, + bool columns) + { + int defCount = definitions.Length; + int[] definitionIndices = DefinitionIndices; + int minCount = 0, maxCount = 0; + double takenSize = 0.0; + double totalStarWeight = 0.0; + int starCount = 0; // number of unresolved *-definitions + double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" + + // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights + double maxStar = 0.0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + ++starCount; + def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" + if (def.UserSize.Value > maxStar) + { + maxStar = def.UserSize.Value; + } + } + } + + if (double.IsPositiveInfinity(maxStar)) + { + // negative scale means one or more of the weights was Infinity + scale = -1.0; + } + else if (starCount > 0) + { + // if maxStar * starCount > double.Max, summing all the weights could cause + // floating-point overflow. To avoid that, scale the weights by a factor to keep + // the sum within limits. Choose a power of 2, to preserve precision. + double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); + if (power < 0.0) + { + scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia + } + } + + + // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights + // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. + // More explanation below... + for (bool runPhase2and3 = true; runPhase2and3;) + { + // Phase 2. Compute total *-weight W and available space S. + // For *-items that have Min or Max constraints, compute the ratios used to decide + // whether proportional space is too big or too small and add the item to the + // corresponding list. (The "min" list is in the first half of definitionIndices, + // the "max" list in the second half. DefinitionIndices has capacity at least + // 2*defCount, so there's room for both lists.) + totalStarWeight = 0.0; + takenSize = 0.0; + minCount = maxCount = 0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + // Debug.Assert(!def.IsShared, "*-defs cannot be shared"); + + if (def.MeasureSize < 0.0) + { + takenSize += -def.MeasureSize; // already resolved + } + else + { + double starWeight = StarWeight(def, scale); + totalStarWeight += starWeight; + + if (def.MinSize > 0.0) + { + // store ratio w/min in MeasureSize (for now) + definitionIndices[minCount++] = i; + def.MeasureSize = starWeight / def.MinSize; + } + + double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + if (!double.IsPositiveInfinity(effectiveMaxSize)) + { + // store ratio w/max in SizeCache (for now) + definitionIndices[defCount + maxCount++] = i; + def.SizeCache = starWeight / effectiveMaxSize; + } + } + } + else + { + double userSize = 0; + + switch (def.UserSize.GridUnitType) + { + case (GridUnitType.Pixel): + userSize = def.UserSize.Value; + break; + + case (GridUnitType.Auto): + userSize = def.MinSize; + break; + } + + double userMaxSize; + + // if (def.IsShared) + // { + // // overriding userMaxSize effectively prevents squishy-ness. + // // this is a "solution" to avoid shared definitions from been sized to + // // different final size at arrange time, if / when different grids receive + // // different final sizes. + // userMaxSize = userSize; + // } + // else + // { + userMaxSize = def.UserMaxSize; + // } + + def.SizeCache = Math.Max(def.MinSize, Math.Min(userSize, userMaxSize)); + takenSize += def.SizeCache; + } + } + + // Phase 3. Resolve *-items whose proportional sizes are too big or too small. + int minCountPhase2 = minCount, maxCountPhase2 = maxCount; + double takenStarWeight = 0.0; + double remainingAvailableSize = finalSize - takenSize; + double remainingStarWeight = totalStarWeight - takenStarWeight; + + MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); + Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); + MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); + Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); + + while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) + { + // the calculation + // remainingStarWeight = totalStarWeight - takenStarWeight + // is subject to catastrophic cancellation if the two terms are nearly equal, + // which leads to meaningless results. Check for that, and recompute from + // the remaining definitions. [This leads to quadratic behavior in really + // pathological cases - but they'd never arise in practice.] + const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate + if (remainingStarWeight < totalStarWeight * starFactor) + { + takenStarWeight = 0.0; + totalStarWeight = 0.0; + + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + if (def.UserSize.IsStar && def.MeasureSize > 0.0) + { + totalStarWeight += StarWeight(def, scale); + } + } + + remainingStarWeight = totalStarWeight - takenStarWeight; + } + + double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? definitions[definitionIndices[defCount + maxCount - 1]].SizeCache : -1.0; + + // choose the def with larger ratio to the current proportion ("max discrepancy") + double proportion = remainingStarWeight / remainingAvailableSize; + bool? chooseMin = Choose(minRatio, maxRatio, proportion); + + // if no def was chosen, advance to phase 4; the current proportion doesn't + // conflict with any min or max values. + if (!(chooseMin.HasValue)) + { + break; + } + + // get the chosen definition and its resolved size + int resolvedIndex; + DefinitionBase resolvedDef; + double resolvedSize; + if (chooseMin == true) + { + resolvedIndex = definitionIndices[minCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = resolvedDef.MinSize; + --minCount; + } + else + { + resolvedIndex = definitionIndices[defCount + maxCount - 1]; + resolvedDef = definitions[resolvedIndex]; + resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + --maxCount; + } + + // resolve the chosen def, deduct its contributions from W and S. + // Defs resolved in phase 3 are marked by storing the negative of their resolved + // size in MeasureSize, to distinguish them from a pending def. + takenSize += resolvedSize; + resolvedDef.MeasureSize = -resolvedSize; + takenStarWeight += StarWeight(resolvedDef, scale); + --starCount; + + remainingAvailableSize = finalSize - takenSize; + remainingStarWeight = totalStarWeight - takenStarWeight; + + // advance to the next candidate defs, removing ones that have been resolved. + // Both counts are advanced, as a def might appear in both lists. + while (minCount > 0 && definitions[definitionIndices[minCount - 1]].MeasureSize < 0.0) + { + --minCount; + definitionIndices[minCount] = -1; + } + while (maxCount > 0 && definitions[definitionIndices[defCount + maxCount - 1]].MeasureSize < 0.0) + { + --maxCount; + definitionIndices[defCount + maxCount] = -1; + } + } + + // decide whether to run Phase2 and Phase3 again. There are 3 cases: + // 1. There is space available, and *-defs remaining. This is the + // normal case - move on to Phase 4 to allocate the remaining + // space proportionally to the remaining *-defs. + // 2. There is space available, but no *-defs. This implies at least one + // def was resolved as 'max', taking less space than its proportion. + // If there are also 'min' defs, reconsider them - we can give + // them more space. If not, all the *-defs are 'max', so there's + // no way to use all the available space. + // 3. We allocated too much space. This implies at least one def was + // resolved as 'min'. If there are also 'max' defs, reconsider + // them, otherwise the over-allocation is an inevitable consequence + // of the given min constraints. + // Note that if we return to Phase2, at least one *-def will have been + // resolved. This guarantees we don't run Phase2+3 infinitely often. + runPhase2and3 = false; + if (starCount == 0 && takenSize < finalSize) + { + // if no *-defs remain and we haven't allocated all the space, reconsider the defs + // resolved as 'min'. Their allocation can be increased to make up the gap. + for (int i = minCount; i < minCountPhase2; ++i) + { + if (definitionIndices[i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + + if (takenSize > finalSize) + { + // if we've allocated too much space, reconsider the defs + // resolved as 'max'. Their allocation can be decreased to make up the gap. + for (int i = maxCount; i < maxCountPhase2; ++i) + { + if (definitionIndices[defCount + i] >= 0) + { + DefinitionBase def = definitions[definitionIndices[defCount + i]]; + def.MeasureSize = 1.0; // mark as 'not yet resolved' + ++starCount; + runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 + } + } + } + } + + // Phase 4. Resolve the remaining defs proportionally. + starCount = 0; + for (int i = 0; i < defCount; ++i) + { + DefinitionBase def = definitions[i]; + + if (def.UserSize.IsStar) + { + if (def.MeasureSize < 0.0) + { + // this def was resolved in phase 3 - fix up its size + def.SizeCache = -def.MeasureSize; + } + else + { + // this def needs resolution, add it to the list, sorted by *-weight + definitionIndices[starCount++] = i; + def.MeasureSize = StarWeight(def, scale); + } + } + } + + if (starCount > 0) + { + StarWeightIndexComparer starWeightIndexComparer = new StarWeightIndexComparer(definitions); + Array.Sort(definitionIndices, 0, starCount, starWeightIndexComparer); + + // compute the partial sums of *-weight, in increasing order of weight + // for minimal loss of precision. + totalStarWeight = 0.0; + for (int i = 0; i < starCount; ++i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + totalStarWeight += def.MeasureSize; + def.SizeCache = totalStarWeight; + } + + // resolve the defs, in decreasing order of weight. + for (int i = starCount - 1; i >= 0; --i) + { + DefinitionBase def = definitions[definitionIndices[i]]; + double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(finalSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; + + // min and max should have no effect by now, but just in case... + resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); + resolvedSize = Math.Max(def.MinSize, resolvedSize); + + // Use the raw (unrounded) sizes to update takenSize, so that + // proportions are computed in the same terms as in phase 3; + // this avoids errors arising from min/max constraints. + takenSize += resolvedSize; + def.SizeCache = resolvedSize; + } + } + + // Phase 5. Apply layout rounding. We do this after fully allocating + // unrounded sizes, to avoid breaking assumptions in the previous phases + if (UseLayoutRounding) + { + var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; + + double[] roundingErrors = RoundingErrors; + double roundedTakenSize = 0.0; + + // round each of the allocated sizes, keeping track of the deltas + for (int i = 0; i < definitions.Length; ++i) + { + DefinitionBase def = definitions[i]; + double roundedSize = RoundLayoutValue(def.SizeCache, dpi); + roundingErrors[i] = (roundedSize - def.SizeCache); + def.SizeCache = roundedSize; + roundedTakenSize += roundedSize; + } + + // The total allocation might differ from finalSize due to rounding + // effects. Tweak the allocations accordingly. + + // Theoretical and historical note. The problem at hand - allocating + // space to columns (or rows) with *-weights, min and max constraints, + // and layout rounding - has a long history. Especially the special + // case of 50 columns with min=1 and available space=435 - allocating + // seats in the U.S. House of Representatives to the 50 states in + // proportion to their population. There are numerous algorithms + // and papers dating back to the 1700's, including the book: + // Balinski, M. and H. Young, Fair Representation, Yale University Press, New Haven, 1982. + // + // One surprising result of all this research is that *any* algorithm + // will suffer from one or more undesirable features such as the + // "population paradox" or the "Alabama paradox", where (to use our terminology) + // increasing the available space by one pixel might actually decrease + // the space allocated to a given column, or increasing the weight of + // a column might decrease its allocation. This is worth knowing + // in case someone complains about this behavior; it's not a bug so + // much as something inherent to the problem. Cite the book mentioned + // above or one of the 100s of references, and resolve as WontFix. + // + // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) + // each being allocated a large number of pixels (~50 or greater), and + // people don't even notice the kind of 1-pixel anomolies that are + // theoretically inevitable, or don't care if they do. At least they shouldn't + // care - no one should be using the results WPF's grid layout to make + // quantitative decisions; its job is to produce a reasonable display, not + // to allocate seats in Congress. + // + // Our algorithm is more susceptible to paradox than the one currently + // used for Congressional allocation ("Huntington-Hill" algorithm), but + // it is faster to run: O(N log N) vs. O(S * N), where N=number of + // definitions, S = number of available pixels. And it produces + // adequate results in practice, as mentioned above. + // + // To reiterate one point: all this only applies when layout rounding + // is in effect. When fractional sizes are allowed, the algorithm + // behaves as well as possible, subject to the min/max constraints + // and precision of floating-point computation. (However, the resulting + // display is subject to anti-aliasing problems. TANSTAAFL.) + + if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) + { + // Compute deltas + for (int i = 0; i < definitions.Length; ++i) + { + definitionIndices[i] = i; + } + + // Sort rounding errors + RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); + Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); + double adjustedSize = roundedTakenSize; + double dpiIncrement = 1.0 / dpi; + + if (roundedTakenSize > finalSize) + { + int i = definitions.Length - 1; + while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache - dpiIncrement; + final = Math.Max(final, definition.MinSize); + if (final < definition.SizeCache) + { + adjustedSize -= dpiIncrement; + } + definition.SizeCache = final; + i--; + } + } + else if (roundedTakenSize < finalSize) + { + int i = 0; + while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Length) + { + DefinitionBase definition = definitions[definitionIndices[i]]; + double final = definition.SizeCache + dpiIncrement; + final = Math.Max(final, definition.MinSize); + if (final > definition.SizeCache) + { + adjustedSize += dpiIncrement; + } + definition.SizeCache = final; + i++; + } + } + } + } + + // Phase 6. Compute final offsets + definitions[0].FinalOffset = 0.0; + for (int i = 0; i < definitions.Length; ++i) + { + definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; + } + } + + // Choose the ratio with maximum discrepancy from the current proportion. + // Returns: + // true if proportion fails a min constraint but not a max, or + // if the min constraint has higher discrepancy + // false if proportion fails a max constraint but not a min, or + // if the max constraint has higher discrepancy + // null if proportion doesn't fail a min or max constraint + // The discrepancy is the ratio of the proportion to the max- or min-ratio. + // When both ratios hit the constraint, minRatio < proportion < maxRatio, + // and the minRatio has higher discrepancy if + // (proportion / minRatio) > (maxRatio / proportion) + private static bool? Choose(double minRatio, double maxRatio, double proportion) + { + if (minRatio < proportion) + { + if (maxRatio > proportion) + { + // compare proportion/minRatio : maxRatio/proportion, but + // do it carefully to avoid floating-point overflow or underflow + // and divide-by-0. + double minPower = Math.Floor(Math.Log(minRatio, 2.0)); + double maxPower = Math.Floor(Math.Log(maxRatio, 2.0)); + double f = Math.Pow(2.0, Math.Floor((minPower + maxPower) / 2.0)); + if ((proportion / f) * (proportion / f) > (minRatio / f) * (maxRatio / f)) + { + return true; + } + else + { + return false; + } + } + else + { + return true; + } + } + else if (maxRatio > proportion) + { + return false; + } + + return null; + } + + /// + /// Sorts row/column indices by rounding error if layout rounding is applied. + /// + /// Index, rounding error pair + /// Index, rounding error pair + /// 1 if x.Value > y.Value, 0 if equal, -1 otherwise + private static int CompareRoundingErrors(KeyValuePair x, KeyValuePair y) + { + if (x.Value < y.Value) + { + return -1; + } + else if (x.Value > y.Value) + { + return 1; + } + return 0; + } + + /// + /// Calculates final (aka arrange) size for given range. + /// + /// Array of definitions to process. + /// Start of the range. + /// Number of items in the range. + /// Final size. + private double GetFinalSizeForRange( + DefinitionBase[] definitions, + int start, + int count) + { + double size = 0; + int i = start + count - 1; + + do + { + size += definitions[i].SizeCache; + } while (--i >= start); + + return (size); + } + + /// + /// Clears dirty state for the grid and its columns / rows + /// + private void SetValid() + { + if (IsTrivialGrid) + { + if (_tempDefinitions != null) + { + // TempDefinitions has to be cleared to avoid "memory leaks" + Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); + _tempDefinitions = null; + } + } + } + + + /// + /// Synchronized ShowGridLines property with the state of the grid's visual collection + /// by adding / removing GridLinesRenderer visual. + /// Returns a reference to GridLinesRenderer visual or null. + /// + private GridLinesRenderer EnsureGridLinesRenderer() + { + // + // synchronize the state + // + if (ShowGridLines && (_gridLinesRenderer == null)) + { + _gridLinesRenderer = new GridLinesRenderer(); + this.VisualChildren.Add(_gridLinesRenderer); + } + + if ((!ShowGridLines) && (_gridLinesRenderer != null)) + { + this.VisualChildren.Remove(_gridLinesRenderer); + _gridLinesRenderer = null; + } + + return (_gridLinesRenderer); + } + + private double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!MathUtilities.AreClose(dpiScale, 1.0)) + { + newValue = Math.Round(value * dpiScale) / dpiScale; + // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. + if (double.IsNaN(newValue) || + double.IsInfinity(newValue) || + MathUtilities.AreClose(newValue, double.MaxValue)) + { + newValue = value; + } + } + else + { + newValue = Math.Round(value); + } + + return newValue; + } + + + private static int ValidateColumn(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Column value."); + } + + return value; + } + + private static int ValidateRow(AvaloniaObject o, int value) + { + if (value < 0) + { + throw new ArgumentException("Invalid Grid.Row value."); + } + + return value; + } + + private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) + { + if (!grid.IsTrivialGrid) // trivial grid is 1 by 1. there is no grid lines anyway + { + grid.Invalidate(); + } + } + + /// + /// Helper for Comparer methods. + /// + /// + /// true if one or both of x and y are null, in which case result holds + /// the relative sort order. + /// + private static bool CompareNullRefs(object x, object y, out int result) + { + result = 2; + + if (x == null) + { + if (y == null) + { + result = 0; + } + else + { + result = -1; + } + } + else + { + if (y == null) + { + result = 1; + } + } + + return (result != 2); + } + + /// + /// Helper accessor to layout time array of definitions. + /// + private DefinitionBase[] TempDefinitions + { + get + { + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; + + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) + { + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); + if (tempDefinitionsWeakRef == null) + { + _tempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); + } + else + { + _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) + { + _tempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = _tempDefinitions; + } + } + } + return (_tempDefinitions); + } + } + + /// + /// Helper accessor to definition indices. + /// + private int[] DefinitionIndices + { + get + { + int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; + + if (_definitionIndices == null || _definitionIndices.Length < requiredLength) + { + _definitionIndices = new int[requiredLength]; + } + + return _definitionIndices; + } + } + + /// + /// Helper accessor to rounding errors. + /// + private double[] RoundingErrors + { + get + { + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); + + if (_roundingErrors == null && requiredLength == 0) + { + _roundingErrors = new double[1]; + } + else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) + { + _roundingErrors = new double[requiredLength]; + } + return _roundingErrors; + } + } + + /// + /// Returns *-weight, adjusted for scale computed during Phase 1 + /// + static double StarWeight(DefinitionBase def, double scale) + { + if (scale < 0.0) + { + // if one of the *-weights is Infinity, adjust the weights by mapping + // Infinty to 1.0 and everything else to 0.0: the infinite items share the + // available space equally, everyone else gets nothing. + return (double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; + } + else + { + return def.UserSize.Value * scale; + } + } + + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } + + /// + /// CellCache stored calculated values of + /// 1. attached cell positioning properties; + /// 2. size type; + /// 3. index of a next cell in the group; + /// + private struct CellCache + { + internal int ColumnIndex; + internal int RowIndex; + internal int ColumnSpan; + internal int RowSpan; + internal LayoutTimeSizeType SizeTypeU; + internal LayoutTimeSizeType SizeTypeV; + internal int Next; + internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } + internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } + internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } + } + + /// + /// Helper class for representing a key for a span in hashtable. + /// + private class SpanKey + { + /// + /// Constructor. + /// + /// Starting index of the span. + /// Span count. + /// true for columns; false for rows. + internal SpanKey(int start, int count, bool u) + { + _start = start; + _count = count; + _u = u; + } + + /// + /// + /// + public override int GetHashCode() + { + int hash = (_start ^ (_count << 2)); + + if (_u) hash &= 0x7ffffff; + else hash |= 0x8000000; + + return (hash); + } + + /// + /// + /// + public override bool Equals(object obj) + { + SpanKey sk = obj as SpanKey; + return (sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u); + } + + /// + /// Returns start index of the span. + /// + internal int Start { get { return (_start); } } + + /// + /// Returns span count. + /// + internal int Count { get { return (_count); } } + + /// + /// Returns true if this is a column span. + /// false if this is a row span. + /// + internal bool U { get { return (_u); } } + + private int _start; + private int _count; + private bool _u; + } + + /// + /// SpanPreferredDistributionOrderComparer. + /// + private class SpanPreferredDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.MinSize.CompareTo(definitionY.MinSize); + } + else + { + result = -1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = +1; + } + else + { + result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); + } + } + } + + return result; + } + } + + /// + /// SpanMaxDistributionOrderComparer. + /// + private class SpanMaxDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + else + { + result = +1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = -1; + } + else + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + } + } + + return result; + } + } + + /// + /// RoundingErrorIndexComparer. + /// + private class RoundingErrorIndexComparer : IComparer + { + private readonly double[] errors; + + internal RoundingErrorIndexComparer(double[] errors) + { + Contract.Requires(errors != null); + this.errors = errors; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + int result; + + if (!CompareNullRefs(indexX, indexY, out result)) + { + double errorX = errors[indexX.Value]; + double errorY = errors[indexY.Value]; + result = errorX.CompareTo(errorY); + } + + return result; + } + } + + /// + /// MinRatioComparer. + /// Sort by w/min (stored in MeasureSize), descending. + /// We query the list from the back, i.e. in ascending order of w/min. + /// + private class MinRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } + + /// + /// MaxRatioComparer. + /// Sort by w/max (stored in SizeCache), ascending. + /// We query the list from the back, i.e. in descending order of w/max. + /// + private class MaxRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// StarWeightComparer. + /// Sort by *-weight (stored in MeasureSize), ascending. + /// + private class StarWeightComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } + + /// + /// MinRatioIndexComparer. + /// + private class MinRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MinRatioIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } + + /// + /// MaxRatioIndexComparer. + /// + private class MaxRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MaxRatioIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } + + /// + /// MaxRatioIndexComparer. + /// + private class StarWeightIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal StarWeightIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } + + /// + /// Helper to render grid lines. + /// + private class GridLinesRenderer : Control + { + /// + /// Static initialization + /// + static GridLinesRenderer() + { + var oddDashArray = new List(); + oddDashArray.Add(_dashLength); + oddDashArray.Add(_dashLength); + var ds1 = new DashStyle(oddDashArray, 0); + _oddDashPen = new Pen(Brushes.Blue, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); + + var evenDashArray = new List(); + evenDashArray.Add(_dashLength); + evenDashArray.Add(_dashLength); + var ds2 = new DashStyle(evenDashArray, 0); + _evenDashPen = new Pen(Brushes.Yellow, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); + } + + /// + /// UpdateRenderBounds. + /// + public override void Render(DrawingContext drawingContext) + { + var grid = this.GetVisualParent(); + + if (grid == null + || !grid.ShowGridLines + || grid.IsTrivialGrid) + { + return; + } + + for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].ActualWidth, 0.0, + grid.ColumnDefinitions[i].ActualWidth, _lastArrangeSize.Height); + } + + for (int i = 1; i < grid.RowDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].ActualHeight, + _lastArrangeSize.Width, grid.RowDefinitions[i].ActualHeight); + } + } + + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) + { + var start = new Point(startX, startY); + var end = new Point(endX, endY); + drawingContext.DrawLine(_oddDashPen, start, end); + drawingContext.DrawLine(_evenDashPen, start, end); + } + + internal void UpdateRenderBounds(Size arrangeSize) + { + _lastArrangeSize = arrangeSize; + this.InvalidateVisual(); + } + + private static Size _lastArrangeSize; + private const double _dashLength = 4.0; // + private const double _penWidth = 1.0; // + private static readonly Pen _oddDashPen; // first pen to draw dash + private static readonly Pen _evenDashPen; // second pen to draw dash + } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Controls/GridWPF.cs b/src/Avalonia.Controls/GridWPF.cs deleted file mode 100644 index 35a38a6423..0000000000 --- a/src/Avalonia.Controls/GridWPF.cs +++ /dev/null @@ -1,2824 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.VisualTree; -using System.Threading; -using JetBrains.Annotations; -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia; -using System.Collections; -using Avalonia.Utilities; -using Avalonia.Layout; - -namespace Avalonia.Controls -{ - /// - /// Grid - /// - public class Grid : Panel - { - internal bool CellsStructureDirty = true; - internal bool SizeToContentU; - internal bool SizeToContentV; - internal bool HasStarCellsU; - internal bool HasStarCellsV; - internal bool HasGroup3CellsInAutoRows; - internal bool ColumnDefinitionsDirty = true; - internal bool RowDefinitionsDirty = true; - - // index of the first cell in first cell group - internal int CellGroup1; - - // index of the first cell in second cell group - internal int CellGroup2; - - // index of the first cell in third cell group - internal int CellGroup3; - - // index of the first cell in fourth cell group - internal int CellGroup4; - - // temporary array used during layout for various purposes - // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) - internal DefinitionBase[] _tempDefinitions; - private GridLinesRenderer _gridLinesRenderer; - - // Keeps track of definition indices. - private int[] _definitionIndices; - - private CellCache[] _cellCache; - - - // Stores unrounded values and rounding errors during layout rounding. - private double[] _roundingErrors; - private DefinitionBase[] _definitionsU; - private DefinitionBase[] _definitionsV; - - // 5 is an arbitrary constant chosen to end the measure loop - private const int _layoutLoopMaxCount = 5; - private static readonly LocalDataStoreSlot _tempDefinitionsDataSlot; - private static readonly IComparer _spanPreferredDistributionOrderComparer; - private static readonly IComparer _spanMaxDistributionOrderComparer; - private static readonly IComparer _minRatioComparer; - private static readonly IComparer _maxRatioComparer; - private static readonly IComparer _starWeightComparer; - - static Grid() - { - ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); - AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); - - _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); - _spanPreferredDistributionOrderComparer = new SpanPreferredDistributionOrderComparer(); - _spanMaxDistributionOrderComparer = new SpanMaxDistributionOrderComparer(); - _minRatioComparer = new MinRatioComparer(); - _maxRatioComparer = new MaxRatioComparer(); - _starWeightComparer = new StarWeightComparer(); - } - - /// - /// Defines the Column attached property. - /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached( - "Column", - validate: ValidateColumn); - - /// - /// Defines the ColumnSpan attached property. - /// - public static readonly AttachedProperty ColumnSpanProperty = - AvaloniaProperty.RegisterAttached("ColumnSpan", 1); - - /// - /// Defines the Row attached property. - /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached( - "Row", - validate: ValidateRow); - - /// - /// Defines the RowSpan attached property. - /// - public static readonly AttachedProperty RowSpanProperty = - AvaloniaProperty.RegisterAttached("RowSpan", 1); - - public static readonly AttachedProperty IsSharedSizeScopeProperty = - AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShowGridLinesProperty = - AvaloniaProperty.Register( - nameof(ShowGridLines), - defaultValue: false); - - /// - /// ShowGridLines property. - /// - public bool ShowGridLines - { - get { return GetValue(ShowGridLinesProperty); } - set { SetValue(ShowGridLinesProperty, value); } - } - - /// - /// Gets the value of the Column attached property for a control. - /// - /// The control. - /// The control's column. - public static int GetColumn(AvaloniaObject element) - { - return element.GetValue(ColumnProperty); - } - - /// - /// Gets the value of the ColumnSpan attached property for a control. - /// - /// The control. - /// The control's column span. - public static int GetColumnSpan(AvaloniaObject element) - { - return element.GetValue(ColumnSpanProperty); - } - - /// - /// Gets the value of the Row attached property for a control. - /// - /// The control. - /// The control's row. - public static int GetRow(AvaloniaObject element) - { - return element.GetValue(RowProperty); - } - - /// - /// Gets the value of the RowSpan attached property for a control. - /// - /// The control. - /// The control's row span. - public static int GetRowSpan(AvaloniaObject element) - { - return element.GetValue(RowSpanProperty); - } - - /// - /// Gets the value of the IsSharedSizeScope attached property for a control. - /// - /// The control. - /// The control's IsSharedSizeScope value. - public static bool GetIsSharedSizeScope(AvaloniaObject element) - { - return element.GetValue(IsSharedSizeScopeProperty); - } - - /// - /// Sets the value of the Column attached property for a control. - /// - /// The control. - /// The column value. - public static void SetColumn(AvaloniaObject element, int value) - { - element.SetValue(ColumnProperty, value); - } - - /// - /// Sets the value of the ColumnSpan attached property for a control. - /// - /// The control. - /// The column span value. - public static void SetColumnSpan(AvaloniaObject element, int value) - { - element.SetValue(ColumnSpanProperty, value); - } - - /// - /// Sets the value of the Row attached property for a control. - /// - /// The control. - /// The row value. - public static void SetRow(AvaloniaObject element, int value) - { - element.SetValue(RowProperty, value); - } - - /// - /// Sets the value of the RowSpan attached property for a control. - /// - /// The control. - /// The row span value. - public static void SetRowSpan(AvaloniaObject element, int value) - { - element.SetValue(RowSpanProperty, value); - } - - private ColumnDefinitions _columnDefinitions; - private RowDefinitions _rowDefinitions; - - /// - /// Gets or sets the columns definitions for the grid. - /// - public ColumnDefinitions ColumnDefinitions - { - get - { - if (_columnDefinitions == null) - { - ColumnDefinitions = new ColumnDefinitions(); - } - - return _columnDefinitions; - } - set - { - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - ColumnDefinitionsDirty = true; - - if (_columnDefinitions.Count > 0) - _definitionsU = _columnDefinitions.Cast().ToArray(); - else - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - - _columnDefinitions.CollectionChanged += (_, e) => - { - if (_columnDefinitions.Count == 0) - { - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - } - else - { - _definitionsU = _columnDefinitions.Cast().ToArray(); - ColumnDefinitionsDirty = true; - } - Invalidate(); - }; - } - } - - /// - /// Gets or sets the row definitions for the grid. - /// - public RowDefinitions RowDefinitions - { - get - { - if (_rowDefinitions == null) - { - RowDefinitions = new RowDefinitions(); - } - - return _rowDefinitions; - } - set - { - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - - RowDefinitionsDirty = true; - - if (_rowDefinitions.Count > 0) - _definitionsV = _rowDefinitions.Cast().ToArray(); - else - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - - _rowDefinitions.CollectionChanged += (_, e) => - { - if (_rowDefinitions.Count == 0) - { - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - } - else - { - _definitionsV = _rowDefinitions.Cast().ToArray(); - RowDefinitionsDirty = true; - } - Invalidate(); - }; - } - } - - private bool IsTrivialGrid => (_definitionsU?.Length <= 1) && - (_definitionsV?.Length <= 1); - - /// - /// Content measurement. - /// - /// Constraint - /// Desired size - protected override Size MeasureOverride(Size constraint) - { - Size gridDesiredSize; - - try - { - if (IsTrivialGrid) - { - gridDesiredSize = new Size(); - - for (int i = 0, count = Children.Count; i < count; ++i) - { - var child = Children[i]; - if (child != null) - { - child.Measure(constraint); - gridDesiredSize = new Size( - Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), - Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); - } - } - } - else - { - { - bool sizeToContentU = double.IsPositiveInfinity(constraint.Width); - bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); - - // Clear index information and rounding errors - if (RowDefinitionsDirty || ColumnDefinitionsDirty) - { - if (_definitionIndices != null) - { - Array.Clear(_definitionIndices, 0, _definitionIndices.Length); - _definitionIndices = null; - } - - if (UseLayoutRounding) - { - if (_roundingErrors != null) - { - Array.Clear(_roundingErrors, 0, _roundingErrors.Length); - _roundingErrors = null; - } - } - } - - ValidateColumnDefinitionsStructure(); - ValidateDefinitionsLayout(_definitionsU, sizeToContentU); - - ValidateRowDefinitionsStructure(); - ValidateDefinitionsLayout(_definitionsV, sizeToContentV); - - CellsStructureDirty |= (SizeToContentU != sizeToContentU) - || (SizeToContentV != sizeToContentV); - - SizeToContentU = sizeToContentU; - SizeToContentV = sizeToContentV; - } - - ValidateCells(); - - Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); - - MeasureCellsGroup(CellGroup1, constraint, false, false); - - { - // after Group1 is measured, only Group3 may have cells belonging to Auto rows. - bool canResolveStarsV = !HasGroup3CellsInAutoRows; - - if (canResolveStarsV) - { - if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, false, false); - if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } - MeasureCellsGroup(CellGroup3, constraint, false, false); - } - else - { - // if at least one cell exists in Group2, it must be measured before - // StarsU can be resolved. - bool canResolveStarsU = CellGroup2 > _cellCache.Length; - if (canResolveStarsU) - { - if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } - MeasureCellsGroup(CellGroup3, constraint, false, false); - if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } - } - else - { - // This is a revision to the algorithm employed for the cyclic - // dependency case described above. We now repeatedly - // measure Group3 and Group2 until their sizes settle. We - // also use a count heuristic to break a loop in case of one. - - bool hasDesiredSizeUChanged = false; - int cnt = 0; - - // Cache Group2MinWidths & Group3MinHeights - double[] group2MinSizes = CacheMinSizes(CellGroup2, false); - double[] group3MinSizes = CacheMinSizes(CellGroup3, true); - - MeasureCellsGroup(CellGroup2, constraint, false, true); - - do - { - if (hasDesiredSizeUChanged) - { - // Reset cached Group3Heights - ApplyCachedMinSizes(group3MinSizes, true); - } - - if (HasStarCellsU) { ResolveStar(_definitionsU, constraint.Width); } - MeasureCellsGroup(CellGroup3, constraint, false, false); - - // Reset cached Group2Widths - ApplyCachedMinSizes(group2MinSizes, false); - - if (HasStarCellsV) { ResolveStar(_definitionsV, constraint.Height); } - MeasureCellsGroup(CellGroup2, constraint, - cnt == _layoutLoopMaxCount, false, out hasDesiredSizeUChanged); - } - while (hasDesiredSizeUChanged && ++cnt <= _layoutLoopMaxCount); - } - } - } - - MeasureCellsGroup(CellGroup4, constraint, false, false); - - gridDesiredSize = new Size( - CalculateDesiredSize(_definitionsU), - CalculateDesiredSize(_definitionsV)); - } - } - finally - { - } - - return (gridDesiredSize); - } - - private void ValidateColumnDefinitionsStructure() - { - if (ColumnDefinitionsDirty) - { - if (_definitionsU == null) - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - ColumnDefinitionsDirty = false; - } - } - - private void ValidateRowDefinitionsStructure() - { - if (RowDefinitionsDirty) - { - if (_definitionsV == null) - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - - RowDefinitionsDirty = false; - } - } - - /// - /// Content arrangement. - /// - /// Arrange size - protected override Size ArrangeOverride(Size arrangeSize) - { - try - { - if (IsTrivialGrid) - { - for (int i = 0, count = Children.Count; i < count; ++i) - { - var child = Children[i]; - if (child != null) - { - child.Arrange(new Rect(arrangeSize)); - } - } - } - else - { - Debug.Assert(_definitionsU.Length > 0 && _definitionsV.Length > 0); - - SetFinalSize(_definitionsU, arrangeSize.Width, true); - SetFinalSize(_definitionsV, arrangeSize.Height, false); - - for (int currentCell = 0; currentCell < _cellCache.Length; ++currentCell) - { - IControl cell = Children[currentCell]; - if (cell == null) - { - continue; - } - - int columnIndex = _cellCache[currentCell].ColumnIndex; - int rowIndex = _cellCache[currentCell].RowIndex; - int columnSpan = _cellCache[currentCell].ColumnSpan; - int rowSpan = _cellCache[currentCell].RowSpan; - - Rect cellRect = new Rect( - columnIndex == 0 ? 0.0 : _definitionsU[columnIndex].FinalOffset, - rowIndex == 0 ? 0.0 : _definitionsV[rowIndex].FinalOffset, - GetFinalSizeForRange(_definitionsU, columnIndex, columnSpan), - GetFinalSizeForRange(_definitionsV, rowIndex, rowSpan)); - - cell.Arrange(cellRect); - } - - // update render bound on grid lines renderer visual - var gridLinesRenderer = EnsureGridLinesRenderer(); - if (gridLinesRenderer != null) - { - gridLinesRenderer.UpdateRenderBounds(arrangeSize); - } - } - } - finally - { - SetValid(); - } - - for (var i = 0; i < ColumnDefinitions.Count; i++) - { - ColumnDefinitions[i].ActualWidth = GetFinalColumnDefinitionWidth(i); - } - - for (var i = 0; i < RowDefinitions.Count; i++) - { - RowDefinitions[i].ActualHeight = GetFinalRowDefinitionHeight(i); - } - - return (arrangeSize); - } - - /// - /// Returns final width for a column. - /// - /// - /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. - /// - internal double GetFinalColumnDefinitionWidth(int columnIndex) - { - double value = 0.0; - - // actual value calculations require structure to be up-to-date - if (!ColumnDefinitionsDirty) - { - value = _definitionsU[(columnIndex + 1) % _definitionsU.Length].FinalOffset; - if (columnIndex != 0) { value -= _definitionsU[columnIndex].FinalOffset; } - } - return (value); - } - - /// - /// Returns final height for a row. - /// - /// - /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. - /// - internal double GetFinalRowDefinitionHeight(int rowIndex) - { - double value = 0.0; - - // actual value calculations require structure to be up-to-date - if (!RowDefinitionsDirty) - { - value = _definitionsV[(rowIndex + 1) % _definitionsV.Length].FinalOffset; - if (rowIndex != 0) { value -= _definitionsV[rowIndex].FinalOffset; } - } - return (value); - } - - /// - /// Invalidates grid caches and makes the grid dirty for measure. - /// - internal void Invalidate() - { - CellsStructureDirty = true; - InvalidateMeasure(); - } - - /// - /// Lays out cells according to rows and columns, and creates lookup grids. - /// - private void ValidateCells() - { - if (!CellsStructureDirty) return; - - _cellCache = new CellCache[Children.Count]; - CellGroup1 = int.MaxValue; - CellGroup2 = int.MaxValue; - CellGroup3 = int.MaxValue; - CellGroup4 = int.MaxValue; - - bool hasStarCellsU = false; - bool hasStarCellsV = false; - bool hasGroup3CellsInAutoRows = false; - - for (int i = _cellCache.Length - 1; i >= 0; --i) - { - var child = Children[i] as Control; - - if (child == null) - { - continue; - } - - var cell = new CellCache(); - - // read indices from the corresponding properties - // clamp to value < number_of_columns - // column >= 0 is guaranteed by property value validation callback - cell.ColumnIndex = Math.Min(GetColumn(child), _definitionsU.Length - 1); - - // clamp to value < number_of_rows - // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(GetRow(child), _definitionsV.Length - 1); - - // read span properties - // clamp to not exceed beyond right side of the grid - // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(GetColumnSpan(child), _definitionsU.Length - cell.ColumnIndex); - - // clamp to not exceed beyond bottom side of the grid - // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(GetRowSpan(child), _definitionsV.Length - cell.RowIndex); - - Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < _definitionsU.Length); - Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < _definitionsV.Length); - - // - // calculate and cache length types for the child - // - cell.SizeTypeU = GetLengthTypeForRange(_definitionsU, cell.ColumnIndex, cell.ColumnSpan); - cell.SizeTypeV = GetLengthTypeForRange(_definitionsV, cell.RowIndex, cell.RowSpan); - - hasStarCellsU |= cell.IsStarU; - hasStarCellsV |= cell.IsStarV; - - // - // distribute cells into four groups. - // - if (!cell.IsStarV) - { - if (!cell.IsStarU) - { - cell.Next = CellGroup1; - CellGroup1 = i; - } - else - { - cell.Next = CellGroup3; - CellGroup3 = i; - - // remember if this cell belongs to auto row - hasGroup3CellsInAutoRows |= cell.IsAutoV; - } - } - else - { - if (cell.IsAutoU - // note below: if spans through Star column it is NOT Auto - && !cell.IsStarU) - { - cell.Next = CellGroup2; - CellGroup2 = i; - } - else - { - cell.Next = CellGroup4; - CellGroup4 = i; - } - } - - _cellCache[i] = cell; - } - - HasStarCellsU = hasStarCellsU; - HasStarCellsV = hasStarCellsV; - HasGroup3CellsInAutoRows = hasGroup3CellsInAutoRows; - - CellsStructureDirty = false; - } - - /// - /// Validates layout time size type information on given array of definitions. - /// Sets MinSize and MeasureSizes. - /// - /// Array of definitions to update. - /// if "true" then star definitions are treated as Auto. - private void ValidateDefinitionsLayout( - DefinitionBase[] definitions, - bool treatStarAsAuto) - { - for (int i = 0; i < definitions.Length; ++i) - { - // Reset minimum size. - definitions[i].MinSize = 0; - - double userMinSize = definitions[i].UserMinSize; - double userMaxSize = definitions[i].UserMaxSize; - double userSize = 0; - - switch (definitions[i].UserSize.GridUnitType) - { - case (GridUnitType.Pixel): - definitions[i].SizeType = LayoutTimeSizeType.Pixel; - userSize = definitions[i].UserSize.Value; - - // this was brought with NewLayout and defeats squishy behavior - userMinSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); - break; - case (GridUnitType.Auto): - definitions[i].SizeType = LayoutTimeSizeType.Auto; - userSize = double.PositiveInfinity; - break; - case (GridUnitType.Star): - if (treatStarAsAuto) - { - definitions[i].SizeType = LayoutTimeSizeType.Auto; - userSize = double.PositiveInfinity; - } - else - { - definitions[i].SizeType = LayoutTimeSizeType.Star; - userSize = double.PositiveInfinity; - } - break; - default: - Debug.Assert(false); - break; - } - - definitions[i].UpdateMinSize(userMinSize); - definitions[i].MeasureSize = Math.Max(userMinSize, Math.Min(userSize, userMaxSize)); - } - } - - private double[] CacheMinSizes(int cellsHead, bool isRows) - { - double[] minSizes = isRows ? new double[_definitionsV.Length] - : new double[_definitionsU.Length]; - - for (int j = 0; j < minSizes.Length; j++) - { - minSizes[j] = -1; - } - - int i = cellsHead; - do - { - if (isRows) - { - minSizes[_cellCache[i].RowIndex] = _definitionsV[_cellCache[i].RowIndex].MinSize; - } - else - { - minSizes[_cellCache[i].ColumnIndex] = _definitionsU[_cellCache[i].ColumnIndex].MinSize; - } - - i = _cellCache[i].Next; - } while (i < _cellCache.Length); - - return minSizes; - } - - private void ApplyCachedMinSizes(double[] minSizes, bool isRows) - { - for (int i = 0; i < minSizes.Length; i++) - { - if (MathUtilities.GreaterThanOrClose(minSizes[i], 0)) - { - if (isRows) - { - _definitionsV[i].MinSize = minSizes[i]; - } - else - { - _definitionsU[i].MinSize = minSizes[i]; - } - } - } - } - - private void MeasureCellsGroup( - int cellsHead, - Size referenceSize, - bool ignoreDesiredSizeU, - bool forceInfinityV) - { - bool unusedHasDesiredSizeUChanged; - MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, - forceInfinityV, out unusedHasDesiredSizeUChanged); - } - - /// - /// Measures one group of cells. - /// - /// Head index of the cells chain. - /// Reference size for spanned cells - /// calculations. - /// When "true" cells' desired - /// width is not registered in columns. - /// Passed through to MeasureCell. - /// When "true" cells' desired height is not registered in rows. - private void MeasureCellsGroup( - int cellsHead, - Size referenceSize, - bool ignoreDesiredSizeU, - bool forceInfinityV, - out bool hasDesiredSizeUChanged) - { - hasDesiredSizeUChanged = false; - - if (cellsHead >= _cellCache.Length) - { - return; - } - - Hashtable spanStore = null; - bool ignoreDesiredSizeV = forceInfinityV; - - int i = cellsHead; - do - { - double oldWidth = Children[i].DesiredSize.Width; - - MeasureCell(i, forceInfinityV); - - hasDesiredSizeUChanged |= !MathUtilities.AreClose(oldWidth, Children[i].DesiredSize.Width); - - if (!ignoreDesiredSizeU) - { - if (_cellCache[i].ColumnSpan == 1) - { - _definitionsU[_cellCache[i].ColumnIndex] - .UpdateMinSize(Math.Min(Children[i].DesiredSize.Width, - _definitionsU[_cellCache[i].ColumnIndex].UserMaxSize)); - } - else - { - RegisterSpan( - ref spanStore, - _cellCache[i].ColumnIndex, - _cellCache[i].ColumnSpan, - true, - Children[i].DesiredSize.Width); - } - } - - if (!ignoreDesiredSizeV) - { - if (_cellCache[i].RowSpan == 1) - { - _definitionsV[_cellCache[i].RowIndex] - .UpdateMinSize(Math.Min(Children[i].DesiredSize.Height, - _definitionsV[_cellCache[i].RowIndex].UserMaxSize)); - } - else - { - RegisterSpan( - ref spanStore, - _cellCache[i].RowIndex, - _cellCache[i].RowSpan, - false, - Children[i].DesiredSize.Height); - } - } - - i = _cellCache[i].Next; - } while (i < _cellCache.Length); - - if (spanStore != null) - { - foreach (DictionaryEntry e in spanStore) - { - SpanKey key = (SpanKey)e.Key; - double requestedSize = (double)e.Value; - - EnsureMinSizeInDefinitionRange( - key.U ? _definitionsU : _definitionsV, - key.Start, - key.Count, - requestedSize, - key.U ? referenceSize.Width : referenceSize.Height); - } - } - } - - /// - /// Helper method to register a span information for delayed processing. - /// - /// Reference to a hashtable object used as storage. - /// Span starting index. - /// Span count. - /// true if this is a column span. false if this is a row span. - /// Value to store. If an entry already exists the biggest value is stored. - private static void RegisterSpan( - ref Hashtable store, - int start, - int count, - bool u, - double value) - { - if (store == null) - { - store = new Hashtable(); - } - - SpanKey key = new SpanKey(start, count, u); - object o = store[key]; - - if (o == null - || value > (double)o) - { - store[key] = value; - } - } - - /// - /// Takes care of measuring a single cell. - /// - /// Index of the cell to measure. - /// If "true" then cell is always - /// calculated to infinite height. - private void MeasureCell( - int cell, - bool forceInfinityV) - { - double cellMeasureWidth; - double cellMeasureHeight; - - if (_cellCache[cell].IsAutoU - && !_cellCache[cell].IsStarU) - { - // if cell belongs to at least one Auto column and not a single Star column - // then it should be calculated "to content", thus it is possible to "shortcut" - // calculations and simply assign PositiveInfinity here. - cellMeasureWidth = double.PositiveInfinity; - } - else - { - // otherwise... - cellMeasureWidth = GetMeasureSizeForRange( - _definitionsU, - _cellCache[cell].ColumnIndex, - _cellCache[cell].ColumnSpan); - } - - if (forceInfinityV) - { - cellMeasureHeight = double.PositiveInfinity; - } - else if (_cellCache[cell].IsAutoV - && !_cellCache[cell].IsStarV) - { - // if cell belongs to at least one Auto row and not a single Star row - // then it should be calculated "to content", thus it is possible to "shortcut" - // calculations and simply assign PositiveInfinity here. - cellMeasureHeight = double.PositiveInfinity; - } - else - { - cellMeasureHeight = GetMeasureSizeForRange( - _definitionsV, - _cellCache[cell].RowIndex, - _cellCache[cell].RowSpan); - } - - var child = Children[cell]; - - if (child != null) - { - Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); - child.Measure(childConstraint); - } - } - - /// - /// Calculates one dimensional measure size for given definitions' range. - /// - /// Source array of definitions to read values from. - /// Starting index of the range. - /// Number of definitions included in the range. - /// Calculated measure size. - /// - /// For "Auto" definitions MinWidth is used in place of PreferredSize. - /// - private double GetMeasureSizeForRange( - DefinitionBase[] definitions, - int start, - int count) - { - Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); - - double measureSize = 0; - int i = start + count - 1; - - do - { - measureSize += (definitions[i].SizeType == LayoutTimeSizeType.Auto) - ? definitions[i].MinSize - : definitions[i].MeasureSize; - } while (--i >= start); - - return (measureSize); - } - - /// - /// Accumulates length type information for given definition's range. - /// - /// Source array of definitions to read values from. - /// Starting index of the range. - /// Number of definitions included in the range. - /// Length type for given range. - private LayoutTimeSizeType GetLengthTypeForRange( - DefinitionBase[] definitions, - int start, - int count) - { - Debug.Assert(0 < count && 0 <= start && (start + count) <= definitions.Length); - - LayoutTimeSizeType lengthType = LayoutTimeSizeType.None; - int i = start + count - 1; - - do - { - lengthType |= definitions[i].SizeType; - } while (--i >= start); - - return (lengthType); - } - - /// - /// Distributes min size back to definition array's range. - /// - /// Start of the range. - /// Number of items in the range. - /// Minimum size that should "fit" into the definitions range. - /// Definition array receiving distribution. - /// Size used to resolve percentages. - private void EnsureMinSizeInDefinitionRange( - DefinitionBase[] definitions, - int start, - int count, - double requestedSize, - double percentReferenceSize) - { - Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Length); - - // avoid processing when asked to distribute "0" - if (!MathUtilities.IsZero(requestedSize)) - { - DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting - int end = start + count; - int autoDefinitionsCount = 0; - double rangeMinSize = 0; - double rangePreferredSize = 0; - double rangeMaxSize = 0; - double maxMaxSize = 0; // maximum of maximum sizes - - // first accumulate the necessary information: - // a) sum up the sizes in the range; - // b) count the number of auto definitions in the range; - // c) initialize temp array - // d) cache the maximum size into SizeCache - // e) accumulate max of max sizes - for (int i = start; i < end; ++i) - { - double minSize = definitions[i].MinSize; - double preferredSize = definitions[i].PreferredSize; - double maxSize = Math.Max(definitions[i].UserMaxSize, minSize); - - rangeMinSize += minSize; - rangePreferredSize += preferredSize; - rangeMaxSize += maxSize; - - definitions[i].SizeCache = maxSize; - - // sanity check: no matter what, but min size must always be the smaller; - // max size must be the biggest; and preferred should be in between - Debug.Assert(minSize <= preferredSize - && preferredSize <= maxSize - && rangeMinSize <= rangePreferredSize - && rangePreferredSize <= rangeMaxSize); - - if (maxMaxSize < maxSize) maxMaxSize = maxSize; - if (definitions[i].UserSize.IsAuto) autoDefinitionsCount++; - tempDefinitions[i - start] = definitions[i]; - } - - // avoid processing if the range already big enough - if (requestedSize > rangeMinSize) - { - if (requestedSize <= rangePreferredSize) - { - // - // requestedSize fits into preferred size of the range. - // distribute according to the following logic: - // * do not distribute into auto definitions - they should continue to stay "tight"; - // * for all non-auto definitions distribute to equi-size min sizes, without exceeding preferred size. - // - // in order to achieve that, definitions are sorted in a way that all auto definitions - // are first, then definitions follow ascending order with PreferredSize as the key of sorting. - // - double sizeToDistribute; - int i; - - Array.Sort(tempDefinitions, 0, count, _spanPreferredDistributionOrderComparer); - for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) - { - // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); - - // adjust sizeToDistribute value by subtracting auto definition min size - sizeToDistribute -= (tempDefinitions[i].MinSize); - } - - for (; i < count; ++i) - { - // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); - - double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); - if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } - sizeToDistribute -= newMinSize; - } - - // sanity check: requested size must all be distributed - Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); - } - else if (requestedSize <= rangeMaxSize) - { - // - // requestedSize bigger than preferred size, but fit into max size of the range. - // distribute according to the following logic: - // * do not distribute into auto definitions, if possible - they should continue to stay "tight"; - // * for all non-auto definitions distribute to euqi-size min sizes, without exceeding max size. - // - // in order to achieve that, definitions are sorted in a way that all non-auto definitions - // are last, then definitions follow ascending order with MaxSize as the key of sorting. - // - double sizeToDistribute; - int i; - - Array.Sort(tempDefinitions, 0, count, _spanMaxDistributionOrderComparer); - for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) - { - // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); - - double preferredSize = tempDefinitions[i].PreferredSize; - double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); - } - - for (; i < count; ++i) - { - // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); - - double preferredSize = tempDefinitions[i].MinSize; - double newMinSize = preferredSize + sizeToDistribute / (count - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); - } - - // sanity check: requested size must all be distributed - Debug.Assert(MathUtilities.IsZero(sizeToDistribute)); - } - else - { - // - // requestedSize bigger than max size of the range. - // distribute according to the following logic: - // * for all definitions distribute to equi-size min sizes. - // - double equalSize = requestedSize / count; - - if (equalSize < maxMaxSize - && !MathUtilities.AreClose(equalSize, maxMaxSize)) - { - // equi-size is less than maximum of maxSizes. - // in this case distribute so that smaller definitions grow faster than - // bigger ones. - double totalRemainingSize = maxMaxSize * count - rangeMaxSize; - double sizeToDistribute = requestedSize - rangeMaxSize; - - // sanity check: totalRemainingSize and sizeToDistribute must be real positive numbers - Debug.Assert(!double.IsInfinity(totalRemainingSize) - && !double.IsNaN(totalRemainingSize) - && totalRemainingSize > 0 - && !double.IsInfinity(sizeToDistribute) - && !double.IsNaN(sizeToDistribute) - && sizeToDistribute > 0); - - for (int i = 0; i < count; ++i) - { - double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; - tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); - } - } - else - { - // - // equi-size is greater or equal to maximum of max sizes. - // all definitions receive equalSize as their mim sizes. - // - for (int i = 0; i < count; ++i) - { - tempDefinitions[i].UpdateMinSize(equalSize); - } - } - } - } - } - } - - // new implementation as of 4.7. Several improvements: - // 1. Allocate to *-defs hitting their min or max constraints, before allocating - // to other *-defs. A def that hits its min uses more space than its - // proportional share, reducing the space available to everyone else. - // The legacy algorithm deducted this space only from defs processed - // after the min; the new algorithm deducts it proportionally from all - // defs. This avoids the "*-defs exceed available space" problem, - // and other related problems where *-defs don't receive proportional - // allocations even though no constraints are preventing it. - // 2. When multiple defs hit min or max, resolve the one with maximum - // discrepancy (defined below). This avoids discontinuities - small - // change in available space resulting in large change to one def's allocation. - // 3. Correct handling of large *-values, including Infinity. - - /// - /// Resolves Star's for given array of definitions. - /// - /// Array of definitions to resolve stars. - /// All available size. - /// - /// Must initialize LayoutSize for all Star entries in given array of definitions. - /// - private void ResolveStar( - DefinitionBase[] definitions, - double availableSize) - { - int defCount = definitions.Length; - DefinitionBase[] tempDefinitions = TempDefinitions; - int minCount = 0, maxCount = 0; - double takenSize = 0; - double totalStarWeight = 0.0; - int starCount = 0; // number of unresolved *-definitions - double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" - - // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights - double maxStar = 0.0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.SizeType == LayoutTimeSizeType.Star) - { - ++starCount; - def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" - if (def.UserSize.Value > maxStar) - { - maxStar = def.UserSize.Value; - } - } - } - - if (double.IsPositiveInfinity(maxStar)) - { - // negative scale means one or more of the weights was Infinity - scale = -1.0; - } - else if (starCount > 0) - { - // if maxStar * starCount > double.Max, summing all the weights could cause - // floating-point overflow. To avoid that, scale the weights by a factor to keep - // the sum within limits. Choose a power of 2, to preserve precision. - double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); - if (power < 0.0) - { - scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia - } - } - - // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights - // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. - // More explanation below... - for (bool runPhase2and3 = true; runPhase2and3;) - { - // Phase 2. Compute total *-weight W and available space S. - // For *-items that have Min or Max constraints, compute the ratios used to decide - // whether proportional space is too big or too small and add the item to the - // corresponding list. (The "min" list is in the first half of tempDefinitions, - // the "max" list in the second half. TempDefinitions has capacity at least - // 2*defCount, so there's room for both lists.) - totalStarWeight = 0.0; - takenSize = 0.0; - minCount = maxCount = 0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - switch (def.SizeType) - { - case (LayoutTimeSizeType.Auto): - takenSize += definitions[i].MinSize; - break; - case (LayoutTimeSizeType.Pixel): - takenSize += def.MeasureSize; - break; - case (LayoutTimeSizeType.Star): - if (def.MeasureSize < 0.0) - { - takenSize += -def.MeasureSize; // already resolved - } - else - { - double starWeight = StarWeight(def, scale); - totalStarWeight += starWeight; - - if (def.MinSize > 0.0) - { - // store ratio w/min in MeasureSize (for now) - tempDefinitions[minCount++] = def; - def.MeasureSize = starWeight / def.MinSize; - } - - double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); - if (!double.IsPositiveInfinity(effectiveMaxSize)) - { - // store ratio w/max in SizeCache (for now) - tempDefinitions[defCount + maxCount++] = def; - def.SizeCache = starWeight / effectiveMaxSize; - } - } - break; - } - } - - // Phase 3. Resolve *-items whose proportional sizes are too big or too small. - int minCountPhase2 = minCount, maxCountPhase2 = maxCount; - double takenStarWeight = 0.0; - double remainingAvailableSize = availableSize - takenSize; - double remainingStarWeight = totalStarWeight - takenStarWeight; - Array.Sort(tempDefinitions, 0, minCount, _minRatioComparer); - Array.Sort(tempDefinitions, defCount, maxCount, _maxRatioComparer); - - while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) - { - // the calculation - // remainingStarWeight = totalStarWeight - takenStarWeight - // is subject to catastrophic cancellation if the two terms are nearly equal, - // which leads to meaningless results. Check for that, and recompute from - // the remaining definitions. [This leads to quadratic behavior in really - // pathological cases - but they'd never arise in practice.] - const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate - if (remainingStarWeight < totalStarWeight * starFactor) - { - takenStarWeight = 0.0; - totalStarWeight = 0.0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - if (def.SizeType == LayoutTimeSizeType.Star && def.MeasureSize > 0.0) - { - totalStarWeight += StarWeight(def, scale); - } - } - - remainingStarWeight = totalStarWeight - takenStarWeight; - } - - double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : double.PositiveInfinity; - double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; - - // choose the def with larger ratio to the current proportion ("max discrepancy") - double proportion = remainingStarWeight / remainingAvailableSize; - bool? chooseMin = Choose(minRatio, maxRatio, proportion); - - // if no def was chosen, advance to phase 4; the current proportion doesn't - // conflict with any min or max values. - if (!(chooseMin.HasValue)) - { - break; - } - - // get the chosen definition and its resolved size - DefinitionBase resolvedDef; - double resolvedSize; - if (chooseMin == true) - { - resolvedDef = tempDefinitions[minCount - 1]; - resolvedSize = resolvedDef.MinSize; - --minCount; - } - else - { - resolvedDef = tempDefinitions[defCount + maxCount - 1]; - resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); - --maxCount; - } - - // resolve the chosen def, deduct its contributions from W and S. - // Defs resolved in phase 3 are marked by storing the negative of their resolved - // size in MeasureSize, to distinguish them from a pending def. - takenSize += resolvedSize; - resolvedDef.MeasureSize = -resolvedSize; - takenStarWeight += StarWeight(resolvedDef, scale); - --starCount; - - remainingAvailableSize = availableSize - takenSize; - remainingStarWeight = totalStarWeight - takenStarWeight; - - // advance to the next candidate defs, removing ones that have been resolved. - // Both counts are advanced, as a def might appear in both lists. - while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) - { - --minCount; - tempDefinitions[minCount] = null; - } - while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) - { - --maxCount; - tempDefinitions[defCount + maxCount] = null; - } - } - - // decide whether to run Phase2 and Phase3 again. There are 3 cases: - // 1. There is space available, and *-defs remaining. This is the - // normal case - move on to Phase 4 to allocate the remaining - // space proportionally to the remaining *-defs. - // 2. There is space available, but no *-defs. This implies at least one - // def was resolved as 'max', taking less space than its proportion. - // If there are also 'min' defs, reconsider them - we can give - // them more space. If not, all the *-defs are 'max', so there's - // no way to use all the available space. - // 3. We allocated too much space. This implies at least one def was - // resolved as 'min'. If there are also 'max' defs, reconsider - // them, otherwise the over-allocation is an inevitable consequence - // of the given min constraints. - // Note that if we return to Phase2, at least one *-def will have been - // resolved. This guarantees we don't run Phase2+3 infinitely often. - runPhase2and3 = false; - if (starCount == 0 && takenSize < availableSize) - { - // if no *-defs remain and we haven't allocated all the space, reconsider the defs - // resolved as 'min'. Their allocation can be increased to make up the gap. - for (int i = minCount; i < minCountPhase2; ++i) - { - DefinitionBase def = tempDefinitions[i]; - if (def != null) - { - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - - if (takenSize > availableSize) - { - // if we've allocated too much space, reconsider the defs - // resolved as 'max'. Their allocation can be decreased to make up the gap. - for (int i = maxCount; i < maxCountPhase2; ++i) - { - DefinitionBase def = tempDefinitions[defCount + i]; - if (def != null) - { - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - } - - // Phase 4. Resolve the remaining defs proportionally. - starCount = 0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.SizeType == LayoutTimeSizeType.Star) - { - if (def.MeasureSize < 0.0) - { - // this def was resolved in phase 3 - fix up its measure size - def.MeasureSize = -def.MeasureSize; - } - else - { - // this def needs resolution, add it to the list, sorted by *-weight - tempDefinitions[starCount++] = def; - def.MeasureSize = StarWeight(def, scale); - } - } - } - - if (starCount > 0) - { - Array.Sort(tempDefinitions, 0, starCount, _starWeightComparer); - - // compute the partial sums of *-weight, in increasing order of weight - // for minimal loss of precision. - totalStarWeight = 0.0; - for (int i = 0; i < starCount; ++i) - { - DefinitionBase def = tempDefinitions[i]; - totalStarWeight += def.MeasureSize; - def.SizeCache = totalStarWeight; - } - - // resolve the defs, in decreasing order of weight - for (int i = starCount - 1; i >= 0; --i) - { - DefinitionBase def = tempDefinitions[i]; - double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; - - // min and max should have no effect by now, but just in case... - resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); - resolvedSize = Math.Max(def.MinSize, resolvedSize); - - def.MeasureSize = resolvedSize; - takenSize += resolvedSize; - } - } - } - - /// - /// Calculates desired size for given array of definitions. - /// - /// Array of definitions to use for calculations. - /// Desired size. - private double CalculateDesiredSize( - DefinitionBase[] definitions) - { - double desiredSize = 0; - - for (int i = 0; i < definitions.Length; ++i) - { - desiredSize += definitions[i].MinSize; - } - - return (desiredSize); - } - - /// - /// Calculates and sets final size for all definitions in the given array. - /// - /// Array of definitions to process. - /// Final size to lay out to. - /// True if sizing row definitions, false for columns - private void SetFinalSize( - DefinitionBase[] definitions, - double finalSize, - bool columns) - { - int defCount = definitions.Length; - int[] definitionIndices = DefinitionIndices; - int minCount = 0, maxCount = 0; - double takenSize = 0.0; - double totalStarWeight = 0.0; - int starCount = 0; // number of unresolved *-definitions - double scale = 1.0; // scale factor applied to each *-weight; negative means "Infinity is present" - - // Phase 1. Determine the maximum *-weight and prepare to adjust *-weights - double maxStar = 0.0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.UserSize.IsStar) - { - ++starCount; - def.MeasureSize = 1.0; // meaning "not yet resolved in phase 3" - if (def.UserSize.Value > maxStar) - { - maxStar = def.UserSize.Value; - } - } - } - - if (double.IsPositiveInfinity(maxStar)) - { - // negative scale means one or more of the weights was Infinity - scale = -1.0; - } - else if (starCount > 0) - { - // if maxStar * starCount > double.Max, summing all the weights could cause - // floating-point overflow. To avoid that, scale the weights by a factor to keep - // the sum within limits. Choose a power of 2, to preserve precision. - double power = Math.Floor(Math.Log(double.MaxValue / maxStar / starCount, 2.0)); - if (power < 0.0) - { - scale = Math.Pow(2.0, power - 4.0); // -4 is just for paranoia - } - } - - - // normally Phases 2 and 3 execute only once. But certain unusual combinations of weights - // and constraints can defeat the algorithm, in which case we repeat Phases 2 and 3. - // More explanation below... - for (bool runPhase2and3 = true; runPhase2and3;) - { - // Phase 2. Compute total *-weight W and available space S. - // For *-items that have Min or Max constraints, compute the ratios used to decide - // whether proportional space is too big or too small and add the item to the - // corresponding list. (The "min" list is in the first half of definitionIndices, - // the "max" list in the second half. DefinitionIndices has capacity at least - // 2*defCount, so there's room for both lists.) - totalStarWeight = 0.0; - takenSize = 0.0; - minCount = maxCount = 0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.UserSize.IsStar) - { - // Debug.Assert(!def.IsShared, "*-defs cannot be shared"); - - if (def.MeasureSize < 0.0) - { - takenSize += -def.MeasureSize; // already resolved - } - else - { - double starWeight = StarWeight(def, scale); - totalStarWeight += starWeight; - - if (def.MinSize > 0.0) - { - // store ratio w/min in MeasureSize (for now) - definitionIndices[minCount++] = i; - def.MeasureSize = starWeight / def.MinSize; - } - - double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); - if (!double.IsPositiveInfinity(effectiveMaxSize)) - { - // store ratio w/max in SizeCache (for now) - definitionIndices[defCount + maxCount++] = i; - def.SizeCache = starWeight / effectiveMaxSize; - } - } - } - else - { - double userSize = 0; - - switch (def.UserSize.GridUnitType) - { - case (GridUnitType.Pixel): - userSize = def.UserSize.Value; - break; - - case (GridUnitType.Auto): - userSize = def.MinSize; - break; - } - - double userMaxSize; - - // if (def.IsShared) - // { - // // overriding userMaxSize effectively prevents squishy-ness. - // // this is a "solution" to avoid shared definitions from been sized to - // // different final size at arrange time, if / when different grids receive - // // different final sizes. - // userMaxSize = userSize; - // } - // else - // { - userMaxSize = def.UserMaxSize; - // } - - def.SizeCache = Math.Max(def.MinSize, Math.Min(userSize, userMaxSize)); - takenSize += def.SizeCache; - } - } - - // Phase 3. Resolve *-items whose proportional sizes are too big or too small. - int minCountPhase2 = minCount, maxCountPhase2 = maxCount; - double takenStarWeight = 0.0; - double remainingAvailableSize = finalSize - takenSize; - double remainingStarWeight = totalStarWeight - takenStarWeight; - - MinRatioIndexComparer minRatioIndexComparer = new MinRatioIndexComparer(definitions); - Array.Sort(definitionIndices, 0, minCount, minRatioIndexComparer); - MaxRatioIndexComparer maxRatioIndexComparer = new MaxRatioIndexComparer(definitions); - Array.Sort(definitionIndices, defCount, maxCount, maxRatioIndexComparer); - - while (minCount + maxCount > 0 && remainingAvailableSize > 0.0) - { - // the calculation - // remainingStarWeight = totalStarWeight - takenStarWeight - // is subject to catastrophic cancellation if the two terms are nearly equal, - // which leads to meaningless results. Check for that, and recompute from - // the remaining definitions. [This leads to quadratic behavior in really - // pathological cases - but they'd never arise in practice.] - const double starFactor = 1.0 / 256.0; // lose more than 8 bits of precision -> recalculate - if (remainingStarWeight < totalStarWeight * starFactor) - { - takenStarWeight = 0.0; - totalStarWeight = 0.0; - - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - if (def.UserSize.IsStar && def.MeasureSize > 0.0) - { - totalStarWeight += StarWeight(def, scale); - } - } - - remainingStarWeight = totalStarWeight - takenStarWeight; - } - - double minRatio = (minCount > 0) ? definitions[definitionIndices[minCount - 1]].MeasureSize : double.PositiveInfinity; - double maxRatio = (maxCount > 0) ? definitions[definitionIndices[defCount + maxCount - 1]].SizeCache : -1.0; - - // choose the def with larger ratio to the current proportion ("max discrepancy") - double proportion = remainingStarWeight / remainingAvailableSize; - bool? chooseMin = Choose(minRatio, maxRatio, proportion); - - // if no def was chosen, advance to phase 4; the current proportion doesn't - // conflict with any min or max values. - if (!(chooseMin.HasValue)) - { - break; - } - - // get the chosen definition and its resolved size - int resolvedIndex; - DefinitionBase resolvedDef; - double resolvedSize; - if (chooseMin == true) - { - resolvedIndex = definitionIndices[minCount - 1]; - resolvedDef = definitions[resolvedIndex]; - resolvedSize = resolvedDef.MinSize; - --minCount; - } - else - { - resolvedIndex = definitionIndices[defCount + maxCount - 1]; - resolvedDef = definitions[resolvedIndex]; - resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); - --maxCount; - } - - // resolve the chosen def, deduct its contributions from W and S. - // Defs resolved in phase 3 are marked by storing the negative of their resolved - // size in MeasureSize, to distinguish them from a pending def. - takenSize += resolvedSize; - resolvedDef.MeasureSize = -resolvedSize; - takenStarWeight += StarWeight(resolvedDef, scale); - --starCount; - - remainingAvailableSize = finalSize - takenSize; - remainingStarWeight = totalStarWeight - takenStarWeight; - - // advance to the next candidate defs, removing ones that have been resolved. - // Both counts are advanced, as a def might appear in both lists. - while (minCount > 0 && definitions[definitionIndices[minCount - 1]].MeasureSize < 0.0) - { - --minCount; - definitionIndices[minCount] = -1; - } - while (maxCount > 0 && definitions[definitionIndices[defCount + maxCount - 1]].MeasureSize < 0.0) - { - --maxCount; - definitionIndices[defCount + maxCount] = -1; - } - } - - // decide whether to run Phase2 and Phase3 again. There are 3 cases: - // 1. There is space available, and *-defs remaining. This is the - // normal case - move on to Phase 4 to allocate the remaining - // space proportionally to the remaining *-defs. - // 2. There is space available, but no *-defs. This implies at least one - // def was resolved as 'max', taking less space than its proportion. - // If there are also 'min' defs, reconsider them - we can give - // them more space. If not, all the *-defs are 'max', so there's - // no way to use all the available space. - // 3. We allocated too much space. This implies at least one def was - // resolved as 'min'. If there are also 'max' defs, reconsider - // them, otherwise the over-allocation is an inevitable consequence - // of the given min constraints. - // Note that if we return to Phase2, at least one *-def will have been - // resolved. This guarantees we don't run Phase2+3 infinitely often. - runPhase2and3 = false; - if (starCount == 0 && takenSize < finalSize) - { - // if no *-defs remain and we haven't allocated all the space, reconsider the defs - // resolved as 'min'. Their allocation can be increased to make up the gap. - for (int i = minCount; i < minCountPhase2; ++i) - { - if (definitionIndices[i] >= 0) - { - DefinitionBase def = definitions[definitionIndices[i]]; - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - - if (takenSize > finalSize) - { - // if we've allocated too much space, reconsider the defs - // resolved as 'max'. Their allocation can be decreased to make up the gap. - for (int i = maxCount; i < maxCountPhase2; ++i) - { - if (definitionIndices[defCount + i] >= 0) - { - DefinitionBase def = definitions[definitionIndices[defCount + i]]; - def.MeasureSize = 1.0; // mark as 'not yet resolved' - ++starCount; - runPhase2and3 = true; // found a candidate, so re-run Phases 2 and 3 - } - } - } - } - - // Phase 4. Resolve the remaining defs proportionally. - starCount = 0; - for (int i = 0; i < defCount; ++i) - { - DefinitionBase def = definitions[i]; - - if (def.UserSize.IsStar) - { - if (def.MeasureSize < 0.0) - { - // this def was resolved in phase 3 - fix up its size - def.SizeCache = -def.MeasureSize; - } - else - { - // this def needs resolution, add it to the list, sorted by *-weight - definitionIndices[starCount++] = i; - def.MeasureSize = StarWeight(def, scale); - } - } - } - - if (starCount > 0) - { - StarWeightIndexComparer starWeightIndexComparer = new StarWeightIndexComparer(definitions); - Array.Sort(definitionIndices, 0, starCount, starWeightIndexComparer); - - // compute the partial sums of *-weight, in increasing order of weight - // for minimal loss of precision. - totalStarWeight = 0.0; - for (int i = 0; i < starCount; ++i) - { - DefinitionBase def = definitions[definitionIndices[i]]; - totalStarWeight += def.MeasureSize; - def.SizeCache = totalStarWeight; - } - - // resolve the defs, in decreasing order of weight. - for (int i = starCount - 1; i >= 0; --i) - { - DefinitionBase def = definitions[definitionIndices[i]]; - double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(finalSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; - - // min and max should have no effect by now, but just in case... - resolvedSize = Math.Min(resolvedSize, def.UserMaxSize); - resolvedSize = Math.Max(def.MinSize, resolvedSize); - - // Use the raw (unrounded) sizes to update takenSize, so that - // proportions are computed in the same terms as in phase 3; - // this avoids errors arising from min/max constraints. - takenSize += resolvedSize; - def.SizeCache = resolvedSize; - } - } - - // Phase 5. Apply layout rounding. We do this after fully allocating - // unrounded sizes, to avoid breaking assumptions in the previous phases - if (UseLayoutRounding) - { - var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; - - double[] roundingErrors = RoundingErrors; - double roundedTakenSize = 0.0; - - // round each of the allocated sizes, keeping track of the deltas - for (int i = 0; i < definitions.Length; ++i) - { - DefinitionBase def = definitions[i]; - double roundedSize = RoundLayoutValue(def.SizeCache, dpi); - roundingErrors[i] = (roundedSize - def.SizeCache); - def.SizeCache = roundedSize; - roundedTakenSize += roundedSize; - } - - // The total allocation might differ from finalSize due to rounding - // effects. Tweak the allocations accordingly. - - // Theoretical and historical note. The problem at hand - allocating - // space to columns (or rows) with *-weights, min and max constraints, - // and layout rounding - has a long history. Especially the special - // case of 50 columns with min=1 and available space=435 - allocating - // seats in the U.S. House of Representatives to the 50 states in - // proportion to their population. There are numerous algorithms - // and papers dating back to the 1700's, including the book: - // Balinski, M. and H. Young, Fair Representation, Yale University Press, New Haven, 1982. - // - // One surprising result of all this research is that *any* algorithm - // will suffer from one or more undesirable features such as the - // "population paradox" or the "Alabama paradox", where (to use our terminology) - // increasing the available space by one pixel might actually decrease - // the space allocated to a given column, or increasing the weight of - // a column might decrease its allocation. This is worth knowing - // in case someone complains about this behavior; it's not a bug so - // much as something inherent to the problem. Cite the book mentioned - // above or one of the 100s of references, and resolve as WontFix. - // - // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) - // each being allocated a large number of pixels (~50 or greater), and - // people don't even notice the kind of 1-pixel anomolies that are - // theoretically inevitable, or don't care if they do. At least they shouldn't - // care - no one should be using the results WPF's grid layout to make - // quantitative decisions; its job is to produce a reasonable display, not - // to allocate seats in Congress. - // - // Our algorithm is more susceptible to paradox than the one currently - // used for Congressional allocation ("Huntington-Hill" algorithm), but - // it is faster to run: O(N log N) vs. O(S * N), where N=number of - // definitions, S = number of available pixels. And it produces - // adequate results in practice, as mentioned above. - // - // To reiterate one point: all this only applies when layout rounding - // is in effect. When fractional sizes are allowed, the algorithm - // behaves as well as possible, subject to the min/max constraints - // and precision of floating-point computation. (However, the resulting - // display is subject to anti-aliasing problems. TANSTAAFL.) - - if (!MathUtilities.AreClose(roundedTakenSize, finalSize)) - { - // Compute deltas - for (int i = 0; i < definitions.Length; ++i) - { - definitionIndices[i] = i; - } - - // Sort rounding errors - RoundingErrorIndexComparer roundingErrorIndexComparer = new RoundingErrorIndexComparer(roundingErrors); - Array.Sort(definitionIndices, 0, definitions.Length, roundingErrorIndexComparer); - double adjustedSize = roundedTakenSize; - double dpiIncrement = 1.0 / dpi; - - if (roundedTakenSize > finalSize) - { - int i = definitions.Length - 1; - while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache - dpiIncrement; - final = Math.Max(final, definition.MinSize); - if (final < definition.SizeCache) - { - adjustedSize -= dpiIncrement; - } - definition.SizeCache = final; - i--; - } - } - else if (roundedTakenSize < finalSize) - { - int i = 0; - while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Length) - { - DefinitionBase definition = definitions[definitionIndices[i]]; - double final = definition.SizeCache + dpiIncrement; - final = Math.Max(final, definition.MinSize); - if (final > definition.SizeCache) - { - adjustedSize += dpiIncrement; - } - definition.SizeCache = final; - i++; - } - } - } - } - - // Phase 6. Compute final offsets - definitions[0].FinalOffset = 0.0; - for (int i = 0; i < definitions.Length; ++i) - { - definitions[(i + 1) % definitions.Length].FinalOffset = definitions[i].FinalOffset + definitions[i].SizeCache; - } - } - - // Choose the ratio with maximum discrepancy from the current proportion. - // Returns: - // true if proportion fails a min constraint but not a max, or - // if the min constraint has higher discrepancy - // false if proportion fails a max constraint but not a min, or - // if the max constraint has higher discrepancy - // null if proportion doesn't fail a min or max constraint - // The discrepancy is the ratio of the proportion to the max- or min-ratio. - // When both ratios hit the constraint, minRatio < proportion < maxRatio, - // and the minRatio has higher discrepancy if - // (proportion / minRatio) > (maxRatio / proportion) - private static bool? Choose(double minRatio, double maxRatio, double proportion) - { - if (minRatio < proportion) - { - if (maxRatio > proportion) - { - // compare proportion/minRatio : maxRatio/proportion, but - // do it carefully to avoid floating-point overflow or underflow - // and divide-by-0. - double minPower = Math.Floor(Math.Log(minRatio, 2.0)); - double maxPower = Math.Floor(Math.Log(maxRatio, 2.0)); - double f = Math.Pow(2.0, Math.Floor((minPower + maxPower) / 2.0)); - if ((proportion / f) * (proportion / f) > (minRatio / f) * (maxRatio / f)) - { - return true; - } - else - { - return false; - } - } - else - { - return true; - } - } - else if (maxRatio > proportion) - { - return false; - } - - return null; - } - - /// - /// Sorts row/column indices by rounding error if layout rounding is applied. - /// - /// Index, rounding error pair - /// Index, rounding error pair - /// 1 if x.Value > y.Value, 0 if equal, -1 otherwise - private static int CompareRoundingErrors(KeyValuePair x, KeyValuePair y) - { - if (x.Value < y.Value) - { - return -1; - } - else if (x.Value > y.Value) - { - return 1; - } - return 0; - } - - /// - /// Calculates final (aka arrange) size for given range. - /// - /// Array of definitions to process. - /// Start of the range. - /// Number of items in the range. - /// Final size. - private double GetFinalSizeForRange( - DefinitionBase[] definitions, - int start, - int count) - { - double size = 0; - int i = start + count - 1; - - do - { - size += definitions[i].SizeCache; - } while (--i >= start); - - return (size); - } - - /// - /// Clears dirty state for the grid and its columns / rows - /// - private void SetValid() - { - if (IsTrivialGrid) - { - if (_tempDefinitions != null) - { - // TempDefinitions has to be cleared to avoid "memory leaks" - Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); - _tempDefinitions = null; - } - } - } - - - /// - /// Synchronized ShowGridLines property with the state of the grid's visual collection - /// by adding / removing GridLinesRenderer visual. - /// Returns a reference to GridLinesRenderer visual or null. - /// - private GridLinesRenderer EnsureGridLinesRenderer() - { - // - // synchronize the state - // - if (ShowGridLines && (_gridLinesRenderer == null)) - { - _gridLinesRenderer = new GridLinesRenderer(); - this.VisualChildren.Add(_gridLinesRenderer); - } - - if ((!ShowGridLines) && (_gridLinesRenderer != null)) - { - this.VisualChildren.Remove(_gridLinesRenderer); - _gridLinesRenderer = null; - } - - return (_gridLinesRenderer); - } - - private double RoundLayoutValue(double value, double dpiScale) - { - double newValue; - - // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.AreClose(dpiScale, 1.0)) - { - newValue = Math.Round(value * dpiScale) / dpiScale; - // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value. - if (double.IsNaN(newValue) || - double.IsInfinity(newValue) || - MathUtilities.AreClose(newValue, double.MaxValue)) - { - newValue = value; - } - } - else - { - newValue = Math.Round(value); - } - - return newValue; - } - - - private static int ValidateColumn(AvaloniaObject o, int value) - { - if (value < 0) - { - throw new ArgumentException("Invalid Grid.Column value."); - } - - return value; - } - - private static int ValidateRow(AvaloniaObject o, int value) - { - if (value < 0) - { - throw new ArgumentException("Invalid Grid.Row value."); - } - - return value; - } - - private static void OnShowGridLinesPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) - { - if (!grid.IsTrivialGrid) // trivial grid is 1 by 1. there is no grid lines anyway - { - grid.Invalidate(); - } - } - - /// - /// Helper for Comparer methods. - /// - /// - /// true if one or both of x and y are null, in which case result holds - /// the relative sort order. - /// - private static bool CompareNullRefs(object x, object y, out int result) - { - result = 2; - - if (x == null) - { - if (y == null) - { - result = 0; - } - else - { - result = -1; - } - } - else - { - if (y == null) - { - result = 1; - } - } - - return (result != 2); - } - - /// - /// Helper accessor to layout time array of definitions. - /// - private DefinitionBase[] TempDefinitions - { - get - { - int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; - - if (_tempDefinitions == null - || _tempDefinitions.Length < requiredLength) - { - WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); - if (tempDefinitionsWeakRef == null) - { - _tempDefinitions = new DefinitionBase[requiredLength]; - Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); - } - else - { - _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; - if (_tempDefinitions == null - || _tempDefinitions.Length < requiredLength) - { - _tempDefinitions = new DefinitionBase[requiredLength]; - tempDefinitionsWeakRef.Target = _tempDefinitions; - } - } - } - return (_tempDefinitions); - } - } - - /// - /// Helper accessor to definition indices. - /// - private int[] DefinitionIndices - { - get - { - int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; - - if (_definitionIndices == null || _definitionIndices.Length < requiredLength) - { - _definitionIndices = new int[requiredLength]; - } - - return _definitionIndices; - } - } - - /// - /// Helper accessor to rounding errors. - /// - private double[] RoundingErrors - { - get - { - int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); - - if (_roundingErrors == null && requiredLength == 0) - { - _roundingErrors = new double[1]; - } - else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) - { - _roundingErrors = new double[requiredLength]; - } - return _roundingErrors; - } - } - - /// - /// Returns *-weight, adjusted for scale computed during Phase 1 - /// - static double StarWeight(DefinitionBase def, double scale) - { - if (scale < 0.0) - { - // if one of the *-weights is Infinity, adjust the weights by mapping - // Infinty to 1.0 and everything else to 0.0: the infinite items share the - // available space equally, everyone else gets nothing. - return (double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; - } - else - { - return def.UserSize.Value * scale; - } - } - - /// - /// LayoutTimeSizeType is used internally and reflects layout-time size type. - /// - [System.Flags] - internal enum LayoutTimeSizeType : byte - { - None = 0x00, - Pixel = 0x01, - Auto = 0x02, - Star = 0x04, - } - - /// - /// CellCache stored calculated values of - /// 1. attached cell positioning properties; - /// 2. size type; - /// 3. index of a next cell in the group; - /// - private struct CellCache - { - internal int ColumnIndex; - internal int RowIndex; - internal int ColumnSpan; - internal int RowSpan; - internal LayoutTimeSizeType SizeTypeU; - internal LayoutTimeSizeType SizeTypeV; - internal int Next; - internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } - internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } - } - - /// - /// Helper class for representing a key for a span in hashtable. - /// - private class SpanKey - { - /// - /// Constructor. - /// - /// Starting index of the span. - /// Span count. - /// true for columns; false for rows. - internal SpanKey(int start, int count, bool u) - { - _start = start; - _count = count; - _u = u; - } - - /// - /// - /// - public override int GetHashCode() - { - int hash = (_start ^ (_count << 2)); - - if (_u) hash &= 0x7ffffff; - else hash |= 0x8000000; - - return (hash); - } - - /// - /// - /// - public override bool Equals(object obj) - { - SpanKey sk = obj as SpanKey; - return (sk != null - && sk._start == _start - && sk._count == _count - && sk._u == _u); - } - - /// - /// Returns start index of the span. - /// - internal int Start { get { return (_start); } } - - /// - /// Returns span count. - /// - internal int Count { get { return (_count); } } - - /// - /// Returns true if this is a column span. - /// false if this is a row span. - /// - internal bool U { get { return (_u); } } - - private int _start; - private int _count; - private bool _u; - } - - /// - /// SpanPreferredDistributionOrderComparer. - /// - private class SpanPreferredDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - if (definitionX.UserSize.IsAuto) - { - if (definitionY.UserSize.IsAuto) - { - result = definitionX.MinSize.CompareTo(definitionY.MinSize); - } - else - { - result = -1; - } - } - else - { - if (definitionY.UserSize.IsAuto) - { - result = +1; - } - else - { - result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); - } - } - } - - return result; - } - } - - /// - /// SpanMaxDistributionOrderComparer. - /// - private class SpanMaxDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - if (definitionX.UserSize.IsAuto) - { - if (definitionY.UserSize.IsAuto) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - else - { - result = +1; - } - } - else - { - if (definitionY.UserSize.IsAuto) - { - result = -1; - } - else - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - } - } - - return result; - } - } - - /// - /// RoundingErrorIndexComparer. - /// - private class RoundingErrorIndexComparer : IComparer - { - private readonly double[] errors; - - internal RoundingErrorIndexComparer(double[] errors) - { - Contract.Requires(errors != null); - this.errors = errors; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - int result; - - if (!CompareNullRefs(indexX, indexY, out result)) - { - double errorX = errors[indexX.Value]; - double errorY = errors[indexY.Value]; - result = errorX.CompareTo(errorY); - } - - return result; - } - } - - /// - /// MinRatioComparer. - /// Sort by w/min (stored in MeasureSize), descending. - /// We query the list from the back, i.e. in ascending order of w/min. - /// - private class MinRatioComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionY, definitionX, out result)) - { - result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); - } - - return result; - } - } - - /// - /// MaxRatioComparer. - /// Sort by w/max (stored in SizeCache), ascending. - /// We query the list from the back, i.e. in descending order of w/max. - /// - private class MaxRatioComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// StarWeightComparer. - /// Sort by *-weight (stored in MeasureSize), ascending. - /// - private class StarWeightComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); - } - - return result; - } - } - - /// - /// MinRatioIndexComparer. - /// - private class MinRatioIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal MinRatioIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionY, definitionX, out result)) - { - result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); - } - - return result; - } - } - - /// - /// MaxRatioIndexComparer. - /// - private class MaxRatioIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal MaxRatioIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// MaxRatioIndexComparer. - /// - private class StarWeightIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal StarWeightIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); - } - - return result; - } - } - - /// - /// Helper to render grid lines. - /// - private class GridLinesRenderer : Control - { - /// - /// Static initialization - /// - static GridLinesRenderer() - { - var oddDashArray = new List(); - oddDashArray.Add(c_dashLength); - oddDashArray.Add(c_dashLength); - var ds1 = new DashStyle(oddDashArray, 0); - s_oddDashPen = new Pen(Brushes.Blue, - c_penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds1); - - var evenDashArray = new List(); - evenDashArray.Add(c_dashLength); - evenDashArray.Add(c_dashLength); - var ds2 = new DashStyle(evenDashArray, 0); - s_evenDashPen = new Pen(Brushes.Yellow, - c_penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds2); - } - - /// - /// UpdateRenderBounds. - /// - public override void Render(DrawingContext drawingContext) - { - var grid = this.GetVisualParent(); - - if (grid == null - || !grid.ShowGridLines - || grid.IsTrivialGrid) - { - return; - } - - for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) - { - DrawGridLine( - drawingContext, - grid.ColumnDefinitions[i].FinalOffset, 0.0, - grid.ColumnDefinitions[i].FinalOffset, lastArrangeSize.Height); - } - - for (int i = 1; i < grid.RowDefinitions.Count; ++i) - { - DrawGridLine( - drawingContext, - 0.0, grid.RowDefinitions[i].FinalOffset, - lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); - } - } - - /// - /// Draw single hi-contrast line. - /// - private static void DrawGridLine( - DrawingContext drawingContext, - double startX, - double startY, - double endX, - double endY) - { - var start = new Point(startX, startY); - var end = new Point(endX, endY); - drawingContext.DrawLine(s_oddDashPen, start, end); - drawingContext.DrawLine(s_evenDashPen, start, end); - } - - internal void UpdateRenderBounds(Size arrangeSize) - { - lastArrangeSize = arrangeSize; - this.InvalidateVisual(); - } - - private static Size lastArrangeSize; - private const double c_dashLength = 4.0; // - private const double c_penWidth = 1.0; // - private static readonly Pen s_oddDashPen; // first pen to draw dash - private static readonly Pen s_evenDashPen; // second pen to draw dash - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/Utils/GridLayout.cs b/src/Avalonia.Controls/Utils/GridLayout.cs deleted file mode 100644 index 7704228a4e..0000000000 --- a/src/Avalonia.Controls/Utils/GridLayout.cs +++ /dev/null @@ -1,705 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Layout; -using JetBrains.Annotations; - -namespace Avalonia.Controls.Utils -{ - /// - /// Contains algorithms that can help to measure and arrange a Grid. - /// - internal class GridLayout - { - /// - /// Initialize a new instance from the column definitions. - /// The instance doesn't care about whether the definitions are rows or columns. - /// It will not calculate the column or row differently. - /// - internal GridLayout([NotNull] ColumnDefinitions columns) - { - if (columns == null) throw new ArgumentNullException(nameof(columns)); - _conventions = columns.Count == 0 - ? new List { new LengthConvention() } - : columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList(); - } - - /// - /// Initialize a new instance from the row definitions. - /// The instance doesn't care about whether the definitions are rows or columns. - /// It will not calculate the column or row differently. - /// - internal GridLayout([NotNull] RowDefinitions rows) - { - if (rows == null) throw new ArgumentNullException(nameof(rows)); - _conventions = rows.Count == 0 - ? new List { new LengthConvention() } - : rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList(); - } - - /// - /// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same. - /// - private const double LayoutTolerance = 1.0 / 256.0; - - /// - /// Gets all the length conventions that come from column/row definitions. - /// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count. - /// - [NotNull] - private readonly List _conventions; - - /// - /// Gets all the length conventions that come from the grid children. - /// - [NotNull] - private readonly List _additionalConventions = - new List(); - - /// - /// Appending these elements into the convention list helps lay them out according to their desired sizes. - /// - /// Some elements are not only in a single grid cell, they have one or more column/row spans, - /// and these elements may affect the grid layout especially the measuring procedure. - /// Append these elements into the convention list can help to layout them correctly through - /// their desired size. Only a small subset of children need to be measured before layout starts - /// and they will be called via the callback. - /// - /// The grid children type. - /// - /// Contains the safe column/row index and its span. - /// Notice that we will not verify whether the range is in the column/row count, - /// so you should get the safe column/row info first. - /// - /// - /// This callback will be called if the thinks that a child should be - /// measured first. Usually, these are the children that have the * or Auto length. - /// - internal void AppendMeasureConventions([NotNull] IDictionary source, - [NotNull] Func getDesiredLength) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength)); - - // M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.) - // Only these columns/rows' layout can be affected by the child desired size. - // - // Find all columns/rows that have Auto or * length. We'll measure the children in advance. - // Only these kind of columns/rows will affect the Grid layout. - // Please note: - // - If the column / row has Auto length, the Grid.DesiredSize and the column width - // will be affected by the child's desired size. - // - If the column / row has* length, the Grid.DesiredSize will be affected by the - // child's desired size but the column width not. - - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // _conventions: | min | max | | | min | | min max | max | - // _additionalC: |<- desired ->| |< desired >| - // _additionalC: |< desired >| |<- desired ->| - - // 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。 - // 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。 - // 请注意: - // - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize; - // - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。 - - // Find all the Auto and * length columns/rows. - var found = new Dictionary(); - for (var i = 0; i < _conventions.Count; i++) - { - var index = i; - var convention = _conventions[index]; - if (convention.Length.IsAuto || convention.Length.IsStar) - { - foreach (var pair in source.Where(x => - x.Value.index <= index && index < x.Value.index + x.Value.span)) - { - found[pair.Key] = pair.Value; - } - } - } - - // Append these layout into the additional convention list. - foreach (var pair in found) - { - var t = pair.Key; - var (index, span) = pair.Value; - var desiredLength = getDesiredLength(t); - if (Math.Abs(desiredLength) > LayoutTolerance) - { - _additionalConventions.Add(new AdditionalLengthConvention(index, span, desiredLength)); - } - } - } - - /// - /// Run measure procedure according to the and gets the . - /// - /// - /// The container length. Usually, it is the constraint of the method. - /// - /// - /// Overriding conventions that allows the algorithm to handle external inputa - /// - /// - /// The measured result that containing the desired size and all the column/row lengths. - /// - [NotNull, Pure] - internal MeasureResult Measure(double containerLength, IReadOnlyList conventions = null) - { - // Prepare all the variables that this method needs to use. - conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList(); - var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - var aggregatedLength = 0.0; - double starUnitLength; - - // M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`. - // We mark the aggregated length as "fix" because we can completely determine their values. Same as below. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // |#fix#| |#fix#| - // - // 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。 - // 我们会将所有能够确定下长度的行列标记为 fix。下同。 - // 请注意: - // - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。 - - aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // M3/7. Fix all the * lengths that have reached the minimum. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // | fix | |#fix#| fix | - - var shouldTestStarMin = true; - while (shouldTestStarMin) - { - // Calculate the unit * length to estimate the length of each column/row that has * length. - // Under this estimated length, check if there is a minimum value that has a length less than its constraint. - // If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value. - // - // 计算单位 * 的长度,以便预估出每一个 * 行列的长度。 - // 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。 - // 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。 - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - foreach (var convention in conventions.Where(x => x.Length.IsStar)) - { - var (star, min) = (convention.Length.Value, convention.MinLength); - var starLength = star * starUnitLength; - if (starLength < min) - { - convention.Fix(min); - starLength = min; - aggregatedLength += starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMin = @fixed; - } - - // M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| | fix |#fix#| fix | fix | - - var shouldTestAuto = true; - while (shouldTestAuto) - { - var @fixed = false; - starUnitLength = (containerLength - aggregatedLength) / starCount; - for (var i = 0; i < conventions.Count; i++) - { - var convention = conventions[i]; - if (!convention.Length.IsAuto) - { - continue; - } - - var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength); - convention.Fix(more); - aggregatedLength += more; - @fixed = true; - break; - } - - shouldTestAuto = @fixed; - } - - // M5/7. Expand the stars according to the additional conventions (usually the child desired length). - // We can't fix this kind of length, so we just mark them as desired (des). - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - - var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions); - aggregatedLength += desiredStarMin; - - // M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength. - // Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#| - // Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure. - // - // desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des) - // greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des - - var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength; - var greedyDesiredLength = aggregatedLength; - - // M7/7. Expand all the rest stars. These stars have no conventions or only have - // max value they can be expanded from zero to constraint. - // - // +-----------------------------------------------------------+ - // | * | A | * | P | A | * | P | * | * | - // +-----------------------------------------------------------+ - // | min | max | | | min | | min max | max | - // |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#| - // Note: This table will be stored as the final result into the MeasureResult. - - var dynamicConvention = ExpandStars(conventions, containerLength); - Clip(dynamicConvention, containerLength); - - // Returns the measuring result. - return new MeasureResult(containerLength, desiredLength, greedyDesiredLength, - conventions, dynamicConvention, minLengths); - } - - /// - /// Run arrange procedure according to the and gets the . - /// - /// - /// The container length. Usually, it is the finalSize of the method. - /// - /// - /// The result that the measuring procedure returns. If it is null, a new measure procedure will run. - /// - /// - /// The measured result that containing the desired size and all the column/row length. - /// - [NotNull, Pure] - public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure) - { - measure = measure ?? Measure(finalLength); - - // If the arrange final length does not equal to the measure length, we should measure again. - if (finalLength - measure.ContainerLength > LayoutTolerance) - { - // If the final length is larger, we will rerun the whole measure. - measure = Measure(finalLength, measure.LeanLengthList); - } - else if (finalLength - measure.ContainerLength < -LayoutTolerance) - { - // If the final length is smaller, we measure the M6/6 procedure only. - var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength); - measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength, - measure.LeanLengthList, dynamicConvention, measure.MinLengths); - } - - return new ArrangeResult(measure.LengthList); - } - - /// - /// Use the to calculate the fixed length of the Auto column/row. - /// - /// The convention list that all the * with minimum length are fixed. - /// The column/row index that should be fixed. - /// The unit * length for the current rest length. - /// The final length of the Auto length column/row. - [Pure] - private double ApplyAdditionalConventionsForAuto(IReadOnlyList conventions, - int index, double starUnitLength) - { - // 1. Calculate all the * length with starUnitLength. - // 2. Exclude all the fixed length and all the * length. - // 3. Compare the rest of the desired length and the convention. - // +-----------------+ - // | * | A | * | - // +-----------------+ - // | exl | | exl | - // |< desired >| - // |< desired >| - - var more = 0.0; - foreach (var additional in _additionalConventions) - { - // If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length. - if (index == additional.Index + additional.Span - 1) - { - var min = Enumerable.Range(additional.Index, additional.Span) - .Select(x => - { - var c = conventions[x]; - if (c.Length.IsAbsolute) return c.Length.Value; - if (c.Length.IsStar) return c.Length.Value * starUnitLength; - return 0.0; - }).Sum(); - more = Math.Max(additional.Min - min, more); - } - } - - return Math.Min(conventions[index].MaxLength, more); - } - - /// - /// Calculate the total desired length of all the * length. - /// Bug Warning: - /// - The behavior of this method is undefined! Different UI Frameworks have different behaviors. - /// - We ignore all the span columns/rows and just take single cells into consideration. - /// - /// All the conventions that have almost been fixed except the rest *. - /// The total desired length of all the * length. - [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] - private (List, double) AggregateAdditionalConventionsForStars( - IReadOnlyList conventions) - { - // 1. Determine all one-span column's desired widths or row's desired heights. - // 2. Order the multi-span conventions by its last index - // (Notice that the sorted data is much smaller than the source.) - // 3. Determine each multi-span last index by calculating the maximum desired size. - - // Before we determine the behavior of this method, we just aggregate the one-span * columns. - - var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - - // Prepare a lengthList variable indicating the fixed length of each column/row. - var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList(); - foreach (var group in _additionalConventions - .Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar) - .ToLookup(x => x.Index)) - { - lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min)); - } - - // Now the lengthList is fixed by every one-span columns/rows. - // Then we should determine the multi-span column's/row's length. - foreach (var group in _additionalConventions - .Where(x => x.Span > 1) - .ToLookup(x => x.Index + x.Span - 1) - // Order the multi-span columns/rows by last index. - .OrderBy(x => x.Key)) - { - var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r])); - lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0); - } - - return (lengthList, lengthList.Sum() - fixedLength); - } - - /// - /// This method implements the last procedure (M7/7) of measure. - /// It expands all the * length to the fixed length according to the . - /// - /// All the conventions that have almost been fixed except the remaining *. - /// The container length. - /// The final pixel length list. - [Pure] - private static List ExpandStars(IEnumerable conventions, double constraint) - { - // Initial. - var dynamicConvention = conventions.Select(x => x.Clone()).ToList(); - constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value); - var starUnitLength = 0.0; - - // M6/6. - if (constraint >= 0) - { - var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value); - - var shouldTestStarMax = true; - while (shouldTestStarMax) - { - var @fixed = false; - starUnitLength = constraint / starCount; - foreach (var convention in dynamicConvention.Where(x => - x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength))) - { - var (star, max) = (convention.Length.Value, convention.MaxLength); - var starLength = star * starUnitLength; - if (starLength > max) - { - convention.Fix(max); - starLength = max; - constraint -= starLength; - starCount -= star; - @fixed = true; - break; - } - } - - shouldTestStarMax = @fixed; - } - } - - Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto)); - - var starUnit = starUnitLength; - var result = dynamicConvention.Select(x => - { - if (x.Length.IsStar) - { - return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value; - } - - return x.Length.Value; - }).ToList(); - - return result; - } - - /// - /// If the container length is not infinity. It may be not enough to contain all the columns/rows. - /// We should clip the columns/rows that have been out of the container bounds. - /// Note: This method may change the items value of . - /// - /// A list of all the column widths and row heights with a fixed pixel length - /// the container length. It can be positive infinity. - private static void Clip([NotNull] IList lengthList, double constraint) - { - if (double.IsInfinity(constraint)) - { - return; - } - - var measureLength = 0.0; - for (var i = 0; i < lengthList.Count; i++) - { - var length = lengthList[i]; - if (constraint - measureLength > length) - { - measureLength += length; - } - else - { - lengthList[i] = constraint - measureLength; - measureLength = constraint; - } - } - } - - /// - /// Contains the convention of each column/row. - /// This is mostly the same as or . - /// We use this because we can treat the column and the row the same. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal class LengthConvention : ICloneable - { - /// - /// Initialize a new instance of . - /// - public LengthConvention() - { - Length = new GridLength(1.0, GridUnitType.Star); - MinLength = 0.0; - MaxLength = double.PositiveInfinity; - } - - /// - /// Initialize a new instance of . - /// - public LengthConvention(GridLength length, double minLength, double maxLength) - { - Length = length; - MinLength = minLength; - MaxLength = maxLength; - if (length.IsAbsolute) - { - _isFixed = true; - } - } - - /// - /// Gets the of a column or a row. - /// - internal GridLength Length { get; private set; } - - /// - /// Gets the minimum convention for a column or a row. - /// - internal double MinLength { get; } - - /// - /// Gets the maximum convention for a column or a row. - /// - internal double MaxLength { get; } - - /// - /// Fix the . - /// If all columns/rows are fixed, we can get the size of all columns/rows in pixels. - /// - /// - /// The pixel length that should be used to fix the convention. - /// - /// - /// If the convention is pixel length, this exception will throw. - /// - public void Fix(double pixel) - { - if (_isFixed) - { - throw new InvalidOperationException("Cannot fix the length convention if it is fixed."); - } - - Length = new GridLength(pixel); - _isFixed = true; - } - - /// - /// Gets a value that indicates whether this convention is fixed. - /// - private bool _isFixed; - - /// - /// Helps the debugger to display the intermediate column/row calculation result. - /// - private string DebuggerDisplay => - $"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]"; - - /// - object ICloneable.Clone() => Clone(); - - /// - /// Get a deep copy of this convention list. - /// We need this because we want to store some intermediate states. - /// - internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength); - } - - /// - /// Contains the convention that comes from the grid children. - /// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions. - /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - internal struct AdditionalLengthConvention - { - /// - /// Initialize a new instance of . - /// - public AdditionalLengthConvention(int index, int span, double min) - { - Index = index; - Span = span; - Min = min; - } - - /// - /// Gets the start index of this additional convention. - /// - public int Index { get; } - - /// - /// Gets the span of this additional convention. - /// - public int Span { get; } - - /// - /// Gets the minimum length of this additional convention. - /// This value is usually provided by the child's desired length. - /// - public double Min { get; } - - /// - /// Helps the debugger to display the intermediate column/row calculation result. - /// - private string DebuggerDisplay => - $"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)"; - } - - /// - /// Stores the result of the measuring procedure. - /// This result can be used to measure children and assign the desired size. - /// Passing this result to can reduce calculation. - /// - [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] - internal class MeasureResult - { - /// - /// Initialize a new instance of . - /// - internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength, - IReadOnlyList leanConventions, IReadOnlyList expandedConventions, IReadOnlyList minLengths) - { - ContainerLength = containerLength; - DesiredLength = desiredLength; - GreedyDesiredLength = greedyDesiredLength; - LeanLengthList = leanConventions; - LengthList = expandedConventions; - MinLengths = minLengths; - } - - /// - /// Gets the container length for this result. - /// This property will be used by to determine whether to measure again or not. - /// - public double ContainerLength { get; } - - /// - /// Gets the desired length of this result. - /// Just return this value as the desired size in . - /// - public double DesiredLength { get; } - - /// - /// Gets the desired length if the container has infinite length. - /// - public double GreedyDesiredLength { get; } - - /// - /// Contains the column/row calculation intermediate result. - /// This value is used by for reducing repeat calculation. - /// - public IReadOnlyList LeanLengthList { get; } - - /// - /// Gets the length list for each column/row. - /// - public IReadOnlyList LengthList { get; } - public IReadOnlyList MinLengths { get; } - } - - /// - /// Stores the result of the measuring procedure. - /// This result can be used to arrange children and assign the render size. - /// - [DebuggerDisplay("{" + nameof(LengthList) + ",nq}")] - internal class ArrangeResult - { - /// - /// Initialize a new instance of . - /// - internal ArrangeResult(IReadOnlyList lengthList) - { - LengthList = lengthList; - } - - /// - /// Gets the length list for each column/row. - /// - public IReadOnlyList LengthList { get; } - } - } -} diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs deleted file mode 100644 index 5f70557385..0000000000 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ /dev/null @@ -1,651 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using Avalonia.Collections; -using Avalonia.Controls.Utils; -using Avalonia.Layout; -using Avalonia.VisualTree; - -namespace Avalonia.Controls -{ - /// - /// Shared size scope implementation. - /// Shares the size information between participating grids. - /// An instance of this class is attached to every that has its - /// IsSharedSizeScope property set to true. - /// - internal sealed class SharedSizeScopeHost : IDisposable - { - private enum MeasurementState - { - Invalidated, - Measuring, - Cached - } - - /// - /// Class containing the measured rows/columns for a single grid. - /// Monitors changes to the row/column collections as well as the SharedSizeGroup changes - /// for the individual items in those collections. - /// Notifies the of SharedSizeGroup changes. - /// - private sealed class MeasurementCache : IDisposable - { - readonly CompositeDisposable _subscriptions; - readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>(); - - public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged; - - public MeasurementCache(Grid grid) - { - Grid = grid; - Results = grid.RowDefinitions.Cast() - .Concat(grid.ColumnDefinitions) - .Select(d => new MeasurementResult(grid, d)) - .ToList(); - - grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged; - grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged; - - - _subscriptions = new CompositeDisposable( - Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged), - grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged), - grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged)); - - } - - // method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid - private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - // route to collection changed as a Reset. - DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - private void DefinitionPropertyChanged(Tuple propertyChanged) - { - if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup)) - { - var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1)); - var oldName = result.SizeGroup?.Name; - var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup; - _groupChanged.OnNext((oldName, newName, result)); - } - } - - private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - int offset = 0; - if (sender is ColumnDefinitions) - offset = Grid.RowDefinitions.Count; - - var newItems = e.NewItems?.OfType().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List(); - var oldItems = e.OldStartingIndex >= 0 - ? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count) - : new List(); - - void NotifyNewItems() - { - foreach (var item in newItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item)); - } - } - - void NotifyOldItems() - { - foreach (var item in oldItems) - { - if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup)) - continue; - - _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item)); - } - } - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - Results.InsertRange(e.NewStartingIndex + offset, newItems); - NotifyNewItems(); - break; - - case NotifyCollectionChangedAction.Remove: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - NotifyOldItems(); - break; - - case NotifyCollectionChangedAction.Move: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, oldItems); - break; - - case NotifyCollectionChangedAction.Replace: - Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count); - Results.InsertRange(e.NewStartingIndex + offset, newItems); - - NotifyOldItems(); - NotifyNewItems(); - - break; - - case NotifyCollectionChangedAction.Reset: - oldItems = Results; - newItems = Results = Grid.RowDefinitions.Cast() - .Concat(Grid.ColumnDefinitions) - .Select(d => new MeasurementResult(Grid, d)) - .ToList(); - NotifyOldItems(); - NotifyNewItems(); - - break; - } - } - - - /// - /// Updates the Results collection with Grid Measure results. - /// - /// Result of the GridLayout.Measure method for the RowDefinitions in the grid. - /// Result of the GridLayout.Measure method for the ColumnDefinitions in the grid. - public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - MeasurementState = MeasurementState.Cached; - for (int i = 0; i < Grid.RowDefinitions.Count; i++) - { - Results[i].MeasuredResult = rowResult.LengthList[i]; - Results[i].MinLength = rowResult.MinLengths[i]; - } - - for (int i = 0; i < Grid.ColumnDefinitions.Count; i++) - { - Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i]; - Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i]; - } - } - - /// - /// Clears the measurement cache, in preparation for the Measure pass. - /// - public void InvalidateMeasure() - { - var newItems = new List(); - var oldItems = new List(); - - MeasurementState = MeasurementState.Invalidated; - - Results.ForEach(r => - { - r.MeasuredResult = double.NaN; - r.SizeGroup?.Reset(); - }); - } - - /// - /// Clears the subscriptions. - /// - public void Dispose() - { - _subscriptions.Dispose(); - _groupChanged.OnCompleted(); - } - - /// - /// Gets the for which this cache has been created. - /// - public Grid Grid { get; } - - /// - /// Gets the of this cache. - /// - public MeasurementState MeasurementState { get; private set; } - - /// - /// Gets the list of instances. - /// - /// - /// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions - /// - public List Results { get; private set; } - } - - - /// - /// Class containing the Measure result for a single Row/Column in a grid. - /// - private class MeasurementResult - { - public MeasurementResult(Grid owningGrid, DefinitionBase definition) - { - OwningGrid = owningGrid; - Definition = definition; - MeasuredResult = double.NaN; - } - - /// - /// Gets the / related to this - /// - public DefinitionBase Definition { get; } - - /// - /// Gets or sets the actual result of the Measure operation for this column. - /// - public double MeasuredResult { get; set; } - - /// - /// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids. - /// - public double MinLength { get; set; } - - /// - /// Gets or sets the that this result belongs to. - /// - public Group SizeGroup { get; set; } - - /// - /// Gets the Grid that is the parent of the Row/Column - /// - public Grid OwningGrid { get; } - - /// - /// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup. - /// - /// A tuple of length and the priority in the shared size group. - public (double length, int priority) GetPriorityLength() - { - var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height; - - if (length.IsAbsolute) - return (MeasuredResult, 1); - if (length.IsAuto) - return (MeasuredResult, 2); - if (MinLength > 0) - return (MinLength, 3); - return (MeasuredResult, 4); - } - } - - /// - /// Visitor class used to gather the final length for a given SharedSizeGroup. - /// - /// - /// The values are applied according to priorities defined in . - /// - private class LentgthGatherer - { - /// - /// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup - /// - public double Length { get; private set; } - private int gatheredPriority = 6; - - /// - /// Visits the applying the result of to its internal cache. - /// - /// The instance to visit. - public void Visit(MeasurementResult result) - { - var (length, priority) = result.GetPriorityLength(); - - if (gatheredPriority < priority) - return; - - gatheredPriority = priority; - if (gatheredPriority == priority) - { - Length = Math.Max(length,Length); - } - else - { - Length = length; - } - } - } - - /// - /// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value. - /// - private class Group - { - private double? cachedResult; - private List _results = new List(); - - /// - /// Gets the name of the SharedSizeGroup. - /// - public string Name { get; } - - public Group(string name) - { - Name = name; - } - - /// - /// Gets the collection of the instances. - /// - public IReadOnlyList Results => _results; - - /// - /// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup. - /// - public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value; - - /// - /// Clears the previously cached result in preparation for measurement. - /// - public void Reset() - { - cachedResult = null; - } - - /// - /// Ads a measurement result to this group and sets it's property - /// to this instance. - /// - /// The to include in this group. - public void Add(MeasurementResult result) - { - if (_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result"); - - result.SizeGroup = this; - _results.Add(result); - } - - /// - /// Removes the measurement result from this group and clears its value. - /// - /// The to clear. - public void Remove(MeasurementResult result) - { - if (!_results.Contains(result)) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result"); - result.SizeGroup = null; - _results.Remove(result); - } - - - private double Gather() - { - var visitor = new LentgthGatherer(); - - _results.ForEach(visitor.Visit); - - return visitor.Length; - } - } - - private readonly AvaloniaList _measurementCaches = new AvaloniaList(); - private readonly Dictionary _groups = new Dictionary(); - private bool _invalidating; - - /// - /// Removes the SharedSizeScope and notifies all affected grids of the change. - /// - public void Dispose() - { - // while (_measurementCaches.Any()) - // _measurementCaches[0].Grid.SharedScopeChanged(); - } - - /// - /// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree. - /// - /// The to add to this scope. - internal void RegisterGrid(Grid toAdd) - { - if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd))) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!"); - - var cache = new MeasurementCache(toAdd); - _measurementCaches.Add(cache); - AddGridToScopes(cache); - } - - /// - /// Removes the registration for a grid in this SharedSizeScope. - /// - /// The to remove. - internal void UnegisterGrid(Grid toRemove) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!"); - - _measurementCaches.Remove(cache); - RemoveGridFromScopes(cache); - cache.Dispose(); - } - - /// - /// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope. - /// - /// The that should be checked. - /// True if the grid should forward its calls. - internal bool ParticipatesInScope(Grid toCheck) - { - return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck)) - ?.Results.Any(r => r.SizeGroup != null) ?? false; - } - - /// - /// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated. - /// Forwards the same call to all affected grids in this scope. - /// - /// The that had it's Measure invalidated. - internal void InvalidateMeasure(Grid grid) - { - // prevent stack overflow - if (_invalidating) - return; - _invalidating = true; - - InvalidateMeasureImpl(grid); - - _invalidating = false; - } - - /// - /// Updates the measurement cache with the results of the measurement pass. - /// - /// The that has been measured. - /// Measurement result for the grid's - /// Measurement result for the grid's - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - if (cache == null) - throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!"); - - cache.UpdateMeasureResult(rowResult, columnResult); - } - - /// - /// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid. - /// - /// The that is being Arranged - /// The 's cached measurement result. - /// The 's cached measurement result. - /// Row and column measurement result updated with the SharedSizeScope constraints. - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - return ( - Arrange(grid.RowDefinitions, rowResult), - Arrange(grid.ColumnDefinitions, columnResult) - ); - } - - /// - /// Invalidates the measure of all grids affected by the SharedSizeGroups contained within. - /// - /// The that is being invalidated. - private void InvalidateMeasureImpl(Grid grid) - { - var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid)); - - if (cache == null) - throw new AvaloniaInternalException( - $"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache"); - - // already invalidated the cache, early out. - if (cache.MeasurementState == MeasurementState.Invalidated) - return; - - // we won't calculate, so we should not invalidate. - if (!ParticipatesInScope(grid)) - return; - - cache.InvalidateMeasure(); - - // maybe there is a condition to only call arrange on some of the calls? - grid.InvalidateMeasure(); - - // find all the scopes within the invalidated grid - var scopeNames = cache.Results - .Where(mr => mr.SizeGroup != null) - .Select(mr => mr.SizeGroup.Name) - .Distinct(); - // find all grids related to those scopes - var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results) - .Select(r => r.OwningGrid) - .Where(g => g.IsMeasureValid) - .Distinct(); - - // invalidate them as well - foreach (var otherGrid in otherGrids) - { - InvalidateMeasureImpl(otherGrid); - } - } - - /// - /// callback notifying the scope that a has changed its - /// SharedSizeGroup - /// - /// Old and New name (either can be null) of the SharedSizeGroup, as well as the result. - private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) - { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); - } - - /// - /// Handles the impact of SharedSizeGroups on the Arrange of / - /// - /// Rows/Columns that were measured - /// The initial measurement result. - /// Modified measure result - private GridLayout.MeasureResult Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) - { - var conventions = measureResult.LeanLengthList.ToList(); - var lengths = measureResult.LengthList.ToList(); - var desiredLength = 0.0; - for (int i = 0; i < definitions.Count; i++) - { - var definition = definitions[i]; - - // for empty SharedSizeGroups pass on unmodified result. - if (string.IsNullOrEmpty(definition.SharedSizeGroup)) - { - desiredLength += measureResult.LengthList[i]; - continue; - } - - var group = _groups[definition.SharedSizeGroup]; - // Length calculated over all Definitions participating in a SharedSizeGroup. - var length = group.CalculatedLength; - - conventions[i] = new GridLayout.LengthConvention( - new GridLength(length), - measureResult.LeanLengthList[i].MinLength, - measureResult.LeanLengthList[i].MaxLength - ); - lengths[i] = length; - desiredLength += length; - } - - return new GridLayout.MeasureResult( - measureResult.ContainerLength, - desiredLength, - measureResult.GreedyDesiredLength,//?? - conventions, - lengths, - measureResult.MinLengths); - } - - /// - /// Adds all measurement results for a grid to their repsective scopes. - /// - /// The for a grid to be added. - private void AddGridToScopes(MeasurementCache cache) - { - cache.GroupChanged.Subscribe(SharedGroupChanged); - - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - AddToGroup(scopeName, result); - } - } - - /// - /// Handles adding the to a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to add the to. - /// The to add to a scope. - private void AddToGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - _groups.Add(scopeName, group = new Group(scopeName)); - - group.Add(result); - } - - /// - /// Removes all measurement results for a grid from their respective scopes. - /// - /// The for a grid to be removed. - private void RemoveGridFromScopes(MeasurementCache cache) - { - foreach (var result in cache.Results) - { - var scopeName = result.Definition.SharedSizeGroup; - RemoveFromGroup(scopeName, result); - } - } - - /// - /// Handles removing the from a SharedSizeGroup. - /// Does nothing for empty SharedSizeGroups. - /// - /// The name (can be null or empty) of the group to remove the from. - /// The to remove from a scope. - private void RemoveFromGroup(string scopeName, MeasurementResult result) - { - if (string.IsNullOrEmpty(scopeName)) - return; - - if (!_groups.TryGetValue(scopeName, out var group)) - throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope"); - - group.Remove(result); - if (!group.Results.Any()) - _groups.Remove(scopeName); - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs b/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs deleted file mode 100644 index 93163f4a92..0000000000 --- a/tests/Avalonia.Controls.UnitTests/GridLayoutTests.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Avalonia.Controls.Utils; -using Xunit; - -namespace Avalonia.Controls.UnitTests -{ - public class GridLayoutTests - { - private const double Inf = double.PositiveInfinity; - - [Theory] - [InlineData("100, 200, 300", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100, 200, 300", 800d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 600d, 600d, new[] { 100d, 200d, 300d })] - [InlineData("100, 200, 300", 400d, 400d, new[] { 100d, 200d, 100d })] - public void MeasureArrange_AllPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,3*", 600d, 0d, new[] { 100d, 200d, 300d })] - public void MeasureArrange_AllStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,2*,3*", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 600d, 100d, new[] { 100d, 200d, 300d })] - [InlineData("100,2*,3*", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,2*,3*", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("100,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("100,200,Auto", 600d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 300d, 300d, new[] { 100d, 200d, 0d })] - [InlineData("100,200,Auto", 200d, 200d, new[] { 100d, 100d, 0d })] - [InlineData("100,200,Auto", 100d, 100d, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 50d, 50d, new[] { 50d, 0d, 0d })] - public void MeasureArrange_MixAutoPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,2*,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,2*,Auto", 600d, 0d, new[] { 200d, 400d, 0d })] - public void MeasureArrange_MixAutoStarLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - [Theory] - [InlineData("*,200,Auto", 0d, 0d, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 600d, 200d, new[] { 400d, 200d, 0d })] - [InlineData("*,200,Auto", 200d, 200d, new[] { 0d, 200d, 0d })] - [InlineData("*,200,Auto", 100d, 100d, new[] { 0d, 100d, 0d })] - public void MeasureArrange_MixAutoStarPixelLength_Correct(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - TestRowDefinitionsOnly(length, containerLength, expectedDesiredLength, expectedLengthList); - } - - - /// - /// This is needed because Mono somehow converts double array to object array in attribute metadata - /// - static void AssertEqual(IList expected, IReadOnlyList actual) - { - var conv = expected.Cast().ToArray(); - Assert.Equal(conv, actual); - } - - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local")] - private static void TestRowDefinitionsOnly(string length, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData("100, 200, 300", 600d, new[] { 100d, 200d, 300d }, new[] { 100d, 200d, 300d })] - [InlineData("*,2*,3*", 0d, new[] { Inf, Inf, Inf }, new[] { 0d, 0d, 0d })] - [InlineData("100,2*,3*", 100d, new[] { 100d, Inf, Inf }, new[] { 100d, 0d, 0d })] - [InlineData("100,200,Auto", 300d, new[] { 100d, 200d, 0d }, new[] { 100d, 200d, 0d })] - [InlineData("*,2*,Auto", 0d, new[] { Inf, Inf, 0d }, new[] { 0d, 0d, 0d })] - [InlineData("*,200,Auto", 200d, new[] { Inf, 200d, 0d }, new[] { 0d, 200d, 0d })] - public void MeasureArrange_InfiniteMeasure_Correct(string length, double expectedDesiredLength, - IList expectedMeasureList, IList expectedArrangeList) - { - // Arrange - var layout = new GridLayout(new RowDefinitions(length)); - - // Measure - Action & Assert - var measure = layout.Measure(Inf); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(measure.DesiredLength, measure); - AssertEqual(expectedArrangeList, arrange.LengthList); - } - - [Theory] - [InlineData("Auto,*,*", new[] { 100d, 100d, 100d }, 600d, 300d, new[] { 100d, 250d, 250d })] - public void MeasureArrange_ChildHasSize_Correct(string length, - IList childLengthList, double containerLength, - double expectedDesiredLength, IList expectedLengthList) - { - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, 1)), - x => (double)childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange(containerLength, measure); - AssertEqual(expectedLengthList, arrange.LengthList); - } - - [Theory] - [InlineData(Inf, 250d, new[] { 100d, Inf, Inf }, new[] { 100d, 50d, 100d })] - [InlineData(400d, 250d, new[] { 100d, 100d, 200d }, new[] { 100d, 100d, 200d })] - [InlineData(325d, 250d, new[] { 100d, 75d, 150d }, new[] { 100d, 75d, 150d })] - [InlineData(250d, 250d, new[] { 100d, 50d, 100d }, new[] { 100d, 50d, 100d })] - [InlineData(160d, 160d, new[] { 100d, 20d, 40d }, new[] { 100d, 20d, 40d })] - public void MeasureArrange_ChildHasSizeAndHasMultiSpan_Correct( - double containerLength, double expectedDesiredLength, - IList expectedMeasureLengthList, IList expectedArrangeLengthList) - { - var length = "100,*,2*"; - var childLengthList = new[] { 150d, 150d, 150d }; - var spans = new[] { 1, 2, 1 }; - - // Arrange - var lengthList = new ColumnDefinitions(length); - var layout = new GridLayout(lengthList); - layout.AppendMeasureConventions( - Enumerable.Range(0, lengthList.Count).ToDictionary(x => x, x => (x, spans[x])), - x => childLengthList[x]); - - // Measure - Action & Assert - var measure = layout.Measure(containerLength); - Assert.Equal(expectedDesiredLength, measure.DesiredLength); - AssertEqual(expectedMeasureLengthList, measure.LengthList); - - // Arrange - Action & Assert - var arrange = layout.Arrange( - double.IsInfinity(containerLength) ? measure.DesiredLength : containerLength, - measure); - AssertEqual(expectedArrangeLengthList, arrange.LengthList); - } - } -} From 7f6414970aadafdb35ec3d632231bafa66f34c30 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 19:47:32 +0800 Subject: [PATCH 055/179] Revert `DrawGridLines` to FinalOffsets --- src/Avalonia.Controls/Grid.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 236b478f95..5b7b4b2afa 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -2779,16 +2779,16 @@ namespace Avalonia.Controls { DrawGridLine( drawingContext, - grid.ColumnDefinitions[i].ActualWidth, 0.0, - grid.ColumnDefinitions[i].ActualWidth, _lastArrangeSize.Height); + 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].ActualHeight, - _lastArrangeSize.Width, grid.RowDefinitions[i].ActualHeight); + 0.0, grid.RowDefinitions[i].FinalOffset, + _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); } } From 9c6c096ea97466ff897dfe47cd31d3eed1ee5801 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 20:45:09 +0800 Subject: [PATCH 056/179] Organize Grid into its own folder and split up its internal classes. --- .../{ => Grid}/DefinitionBase.cs | 6 +- src/Avalonia.Controls/{ => Grid}/Grid.cs | 513 +----------------- src/Avalonia.Controls/Grid/GridCellCache.cs | 26 + .../{ => Grid}/GridLength.cs | 0 .../Grid/GridLinesRenderer.cs | 95 ++++ src/Avalonia.Controls/Grid/GridSpanKey.cs | 69 +++ .../{ => Grid}/GridSplitter.cs | 0 .../Grid/LayoutTimeSizeType.cs | 17 + .../Grid/MaxRatioComparer.cs | 31 ++ .../Grid/MaxRatioIndexComparer.cs | 46 ++ .../Grid/MinRatioComparer.cs | 30 + .../Grid/MinRatioIndexComparer.cs | 46 ++ .../Grid/RoundingErrorIndexComparer.cs | 36 ++ .../Grid/SpanMaxDistributionOrderComparer.cs | 46 ++ .../SpanPreferredDistributionOrderComparer.cs | 46 ++ .../Grid/StarWeightComparer.cs | 29 + .../Grid/StarWeightIndexComparer.cs | 47 ++ 17 files changed, 574 insertions(+), 509 deletions(-) rename src/Avalonia.Controls/{ => Grid}/DefinitionBase.cs (94%) rename src/Avalonia.Controls/{ => Grid}/Grid.cs (85%) create mode 100644 src/Avalonia.Controls/Grid/GridCellCache.cs rename src/Avalonia.Controls/{ => Grid}/GridLength.cs (100%) create mode 100644 src/Avalonia.Controls/Grid/GridLinesRenderer.cs create mode 100644 src/Avalonia.Controls/Grid/GridSpanKey.cs rename src/Avalonia.Controls/{ => Grid}/GridSplitter.cs (100%) create mode 100644 src/Avalonia.Controls/Grid/LayoutTimeSizeType.cs create mode 100644 src/Avalonia.Controls/Grid/MaxRatioComparer.cs create mode 100644 src/Avalonia.Controls/Grid/MaxRatioIndexComparer.cs create mode 100644 src/Avalonia.Controls/Grid/MinRatioComparer.cs create mode 100644 src/Avalonia.Controls/Grid/MinRatioIndexComparer.cs create mode 100644 src/Avalonia.Controls/Grid/RoundingErrorIndexComparer.cs create mode 100644 src/Avalonia.Controls/Grid/SpanMaxDistributionOrderComparer.cs create mode 100644 src/Avalonia.Controls/Grid/SpanPreferredDistributionOrderComparer.cs create mode 100644 src/Avalonia.Controls/Grid/StarWeightComparer.cs create mode 100644 src/Avalonia.Controls/Grid/StarWeightIndexComparer.cs diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/Grid/DefinitionBase.cs similarity index 94% rename from src/Avalonia.Controls/DefinitionBase.cs rename to src/Avalonia.Controls/Grid/DefinitionBase.cs index 3dd4819ca7..ad2f33a18a 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/Grid/DefinitionBase.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls /// /// Layout-time user size type. /// - internal Grid.LayoutTimeSizeType SizeType { get; set; } + internal LayoutTimeSizeType SizeType { get; set; } /// /// Returns or sets measure size for the definition. @@ -65,12 +65,12 @@ namespace Avalonia.Controls get { double preferredSize = MinSize; - if (SizeType != Grid.LayoutTimeSizeType.Auto + if (SizeType != LayoutTimeSizeType.Auto && preferredSize < MeasureSize) { preferredSize = MeasureSize; } - return (preferredSize); + return (preferredSize); } } diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs similarity index 85% rename from src/Avalonia.Controls/Grid.cs rename to src/Avalonia.Controls/Grid/Grid.cs index 5b7b4b2afa..a14e3f0d01 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -9,11 +9,9 @@ using System.Reactive.Linq; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; -using Avalonia.VisualTree; using System.Threading; using JetBrains.Annotations; using Avalonia.Controls; -using Avalonia.Media; using Avalonia; using System.Collections; using Avalonia.Utilities; @@ -21,9 +19,6 @@ using Avalonia.Layout; namespace Avalonia.Controls { - /// - /// Grid - /// public class Grid : Panel { internal bool CellsStructureDirty = true; @@ -55,8 +50,7 @@ namespace Avalonia.Controls // Keeps track of definition indices. private int[] _definitionIndices; - private CellCache[] _cellCache; - + private GridCellCache[] _cellCache; // Stores unrounded values and rounding errors during layout rounding. private double[] _roundingErrors; @@ -309,7 +303,7 @@ namespace Avalonia.Controls } } - private bool IsTrivialGrid => (_definitionsU?.Length <= 1) && + internal bool IsTrivialGrid => (_definitionsU?.Length <= 1) && (_definitionsV?.Length <= 1); /// @@ -607,7 +601,7 @@ namespace Avalonia.Controls { if (!CellsStructureDirty) return; - _cellCache = new CellCache[Children.Count]; + _cellCache = new GridCellCache[Children.Count]; CellGroup1 = int.MaxValue; CellGroup2 = int.MaxValue; CellGroup3 = int.MaxValue; @@ -626,7 +620,7 @@ namespace Avalonia.Controls continue; } - var cell = new CellCache(); + var cell = new GridCellCache(); // read indices from the corresponding properties // clamp to value < number_of_columns @@ -895,7 +889,7 @@ namespace Avalonia.Controls { foreach (DictionaryEntry e in spanStore) { - SpanKey key = (SpanKey)e.Key; + GridSpanKey key = (GridSpanKey)e.Key; double requestedSize = (double)e.Value; EnsureMinSizeInDefinitionRange( @@ -928,7 +922,7 @@ namespace Avalonia.Controls store = new Hashtable(); } - SpanKey key = new SpanKey(start, count, u); + GridSpanKey key = new GridSpanKey(start, count, u); object o = store[key]; if (o == null @@ -2127,7 +2121,6 @@ namespace Avalonia.Controls } } - /// /// Synchronized ShowGridLines property with the state of the grid's visual collection /// by adding / removing GridLinesRenderer visual. @@ -2213,7 +2206,7 @@ namespace Avalonia.Controls /// true if one or both of x and y are null, in which case result holds /// the relative sort order. /// - private static bool CompareNullRefs(object x, object y, out int result) + internal static bool CompareNullRefs(object x, object y, out int result) { result = 2; @@ -2328,497 +2321,5 @@ namespace Avalonia.Controls return def.UserSize.Value * scale; } } - - /// - /// LayoutTimeSizeType is used internally and reflects layout-time size type. - /// - [System.Flags] - internal enum LayoutTimeSizeType : byte - { - None = 0x00, - Pixel = 0x01, - Auto = 0x02, - Star = 0x04, - } - - /// - /// CellCache stored calculated values of - /// 1. attached cell positioning properties; - /// 2. size type; - /// 3. index of a next cell in the group; - /// - private struct CellCache - { - internal int ColumnIndex; - internal int RowIndex; - internal int ColumnSpan; - internal int RowSpan; - internal LayoutTimeSizeType SizeTypeU; - internal LayoutTimeSizeType SizeTypeV; - internal int Next; - internal bool IsStarU { get { return ((SizeTypeU & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoU { get { return ((SizeTypeU & LayoutTimeSizeType.Auto) != 0); } } - internal bool IsStarV { get { return ((SizeTypeV & LayoutTimeSizeType.Star) != 0); } } - internal bool IsAutoV { get { return ((SizeTypeV & LayoutTimeSizeType.Auto) != 0); } } - } - - /// - /// Helper class for representing a key for a span in hashtable. - /// - private class SpanKey - { - /// - /// Constructor. - /// - /// Starting index of the span. - /// Span count. - /// true for columns; false for rows. - internal SpanKey(int start, int count, bool u) - { - _start = start; - _count = count; - _u = u; - } - - /// - /// - /// - public override int GetHashCode() - { - int hash = (_start ^ (_count << 2)); - - if (_u) hash &= 0x7ffffff; - else hash |= 0x8000000; - - return (hash); - } - - /// - /// - /// - public override bool Equals(object obj) - { - SpanKey sk = obj as SpanKey; - return (sk != null - && sk._start == _start - && sk._count == _count - && sk._u == _u); - } - - /// - /// Returns start index of the span. - /// - internal int Start { get { return (_start); } } - - /// - /// Returns span count. - /// - internal int Count { get { return (_count); } } - - /// - /// Returns true if this is a column span. - /// false if this is a row span. - /// - internal bool U { get { return (_u); } } - - private int _start; - private int _count; - private bool _u; - } - - /// - /// SpanPreferredDistributionOrderComparer. - /// - private class SpanPreferredDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - if (definitionX.UserSize.IsAuto) - { - if (definitionY.UserSize.IsAuto) - { - result = definitionX.MinSize.CompareTo(definitionY.MinSize); - } - else - { - result = -1; - } - } - else - { - if (definitionY.UserSize.IsAuto) - { - result = +1; - } - else - { - result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); - } - } - } - - return result; - } - } - - /// - /// SpanMaxDistributionOrderComparer. - /// - private class SpanMaxDistributionOrderComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - if (definitionX.UserSize.IsAuto) - { - if (definitionY.UserSize.IsAuto) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - else - { - result = +1; - } - } - else - { - if (definitionY.UserSize.IsAuto) - { - result = -1; - } - else - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - } - } - - return result; - } - } - - /// - /// RoundingErrorIndexComparer. - /// - private class RoundingErrorIndexComparer : IComparer - { - private readonly double[] errors; - - internal RoundingErrorIndexComparer(double[] errors) - { - Contract.Requires(errors != null); - this.errors = errors; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - int result; - - if (!CompareNullRefs(indexX, indexY, out result)) - { - double errorX = errors[indexX.Value]; - double errorY = errors[indexY.Value]; - result = errorX.CompareTo(errorY); - } - - return result; - } - } - - /// - /// MinRatioComparer. - /// Sort by w/min (stored in MeasureSize), descending. - /// We query the list from the back, i.e. in ascending order of w/min. - /// - private class MinRatioComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionY, definitionX, out result)) - { - result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); - } - - return result; - } - } - - /// - /// MaxRatioComparer. - /// Sort by w/max (stored in SizeCache), ascending. - /// We query the list from the back, i.e. in descending order of w/max. - /// - private class MaxRatioComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// StarWeightComparer. - /// Sort by *-weight (stored in MeasureSize), ascending. - /// - private class StarWeightComparer : IComparer - { - public int Compare(object x, object y) - { - DefinitionBase definitionX = x as DefinitionBase; - DefinitionBase definitionY = y as DefinitionBase; - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); - } - - return result; - } - } - - /// - /// MinRatioIndexComparer. - /// - private class MinRatioIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal MinRatioIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionY, definitionX, out result)) - { - result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); - } - - return result; - } - } - - /// - /// MaxRatioIndexComparer. - /// - private class MaxRatioIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal MaxRatioIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); - } - - return result; - } - } - - /// - /// MaxRatioIndexComparer. - /// - private class StarWeightIndexComparer : IComparer - { - private readonly DefinitionBase[] definitions; - - internal StarWeightIndexComparer(DefinitionBase[] definitions) - { - Contract.Requires(definitions != null); - this.definitions = definitions; - } - - public int Compare(object x, object y) - { - int? indexX = x as int?; - int? indexY = y as int?; - - DefinitionBase definitionX = null; - DefinitionBase definitionY = null; - - if (indexX != null) - { - definitionX = definitions[indexX.Value]; - } - if (indexY != null) - { - definitionY = definitions[indexY.Value]; - } - - int result; - - if (!CompareNullRefs(definitionX, definitionY, out result)) - { - result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); - } - - return result; - } - } - - /// - /// Helper to render grid lines. - /// - private class GridLinesRenderer : Control - { - /// - /// Static initialization - /// - static GridLinesRenderer() - { - var oddDashArray = new List(); - oddDashArray.Add(_dashLength); - oddDashArray.Add(_dashLength); - var ds1 = new DashStyle(oddDashArray, 0); - _oddDashPen = new Pen(Brushes.Blue, - _penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds1); - - var evenDashArray = new List(); - evenDashArray.Add(_dashLength); - evenDashArray.Add(_dashLength); - var ds2 = new DashStyle(evenDashArray, 0); - _evenDashPen = new Pen(Brushes.Yellow, - _penWidth, - lineCap: PenLineCap.Flat, - dashStyle: ds2); - } - - /// - /// UpdateRenderBounds. - /// - public override void Render(DrawingContext drawingContext) - { - var grid = this.GetVisualParent(); - - if (grid == null - || !grid.ShowGridLines - || grid.IsTrivialGrid) - { - return; - } - - for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) - { - DrawGridLine( - drawingContext, - grid.ColumnDefinitions[i].FinalOffset, 0.0, - grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height); - } - - for (int i = 1; i < grid.RowDefinitions.Count; ++i) - { - DrawGridLine( - drawingContext, - 0.0, grid.RowDefinitions[i].FinalOffset, - _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); - } - } - - /// - /// Draw single hi-contrast line. - /// - private static void DrawGridLine( - DrawingContext drawingContext, - double startX, - double startY, - double endX, - double endY) - { - var start = new Point(startX, startY); - var end = new Point(endX, endY); - drawingContext.DrawLine(_oddDashPen, start, end); - drawingContext.DrawLine(_evenDashPen, start, end); - } - - internal void UpdateRenderBounds(Size arrangeSize) - { - _lastArrangeSize = arrangeSize; - this.InvalidateVisual(); - } - - private static Size _lastArrangeSize; - private const double _dashLength = 4.0; // - private const double _penWidth = 1.0; // - private static readonly Pen _oddDashPen; // first pen to draw dash - private static readonly Pen _evenDashPen; // second pen to draw dash - } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/GridCellCache.cs b/src/Avalonia.Controls/Grid/GridCellCache.cs new file mode 100644 index 0000000000..81edf72ca5 --- /dev/null +++ b/src/Avalonia.Controls/Grid/GridCellCache.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Avalonia.Controls +{ + /// + /// CellCache stored calculated values of + /// 1. attached cell positioning properties; + /// 2. size type; + /// 3. index of a next cell in the group; + /// + internal struct GridCellCache + { + 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); } } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/Grid/GridLength.cs similarity index 100% rename from src/Avalonia.Controls/GridLength.cs rename to src/Avalonia.Controls/Grid/GridLength.cs diff --git a/src/Avalonia.Controls/Grid/GridLinesRenderer.cs b/src/Avalonia.Controls/Grid/GridLinesRenderer.cs new file mode 100644 index 0000000000..338b8c40c1 --- /dev/null +++ b/src/Avalonia.Controls/Grid/GridLinesRenderer.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Avalonia.VisualTree; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + internal class GridLinesRenderer : Control + { + /// + /// Static initialization + /// + static GridLinesRenderer() + { + var oddDashArray = new List(); + oddDashArray.Add(_dashLength); + oddDashArray.Add(_dashLength); + var ds1 = new DashStyle(oddDashArray, 0); + _oddDashPen = new Pen(Brushes.Blue, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds1); + + var evenDashArray = new List(); + evenDashArray.Add(_dashLength); + evenDashArray.Add(_dashLength); + var ds2 = new DashStyle(evenDashArray, 0); + _evenDashPen = new Pen(Brushes.Yellow, + _penWidth, + lineCap: PenLineCap.Flat, + dashStyle: ds2); + } + + /// + /// UpdateRenderBounds. + /// + public override void Render(DrawingContext drawingContext) + { + var grid = this.GetVisualParent(); + + if (grid == null + || !grid.ShowGridLines + || grid.IsTrivialGrid) + { + return; + } + + for (int i = 1; i < grid.ColumnDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].FinalOffset, 0.0, + grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height); + } + + for (int i = 1; i < grid.RowDefinitions.Count; ++i) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].FinalOffset, + _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); + } + } + + /// + /// Draw single hi-contrast line. + /// + private static void DrawGridLine( + DrawingContext drawingContext, + double startX, + double startY, + double endX, + double endY) + { + var start = new Point(startX, startY); + var end = new Point(endX, endY); + drawingContext.DrawLine(_oddDashPen, start, end); + drawingContext.DrawLine(_evenDashPen, start, end); + } + + internal void UpdateRenderBounds(Size arrangeSize) + { + _lastArrangeSize = arrangeSize; + this.InvalidateVisual(); + } + + private static Size _lastArrangeSize; + private const double _dashLength = 4.0; // + private const double _penWidth = 1.0; // + private static readonly Pen _oddDashPen; // first pen to draw dash + private static readonly Pen _evenDashPen; // second pen to draw dash + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/GridSpanKey.cs b/src/Avalonia.Controls/Grid/GridSpanKey.cs new file mode 100644 index 0000000000..cd48bc1265 --- /dev/null +++ b/src/Avalonia.Controls/Grid/GridSpanKey.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Avalonia.Controls +{ + /// + /// Helper class for representing a key for a span in hashtable. + /// + internal class GridSpanKey + { + /// + /// Constructor. + /// + /// Starting index of the span. + /// Span count. + /// true for columns; false for rows. + internal GridSpanKey(int start, int count, bool u) + { + _start = start; + _count = count; + _u = u; + } + + /// + /// + /// + public override int GetHashCode() + { + int hash = (_start ^ (_count << 2)); + + if (_u) hash &= 0x7ffffff; + else hash |= 0x8000000; + + return (hash); + } + + /// + /// + /// + public override bool Equals(object obj) + { + GridSpanKey sk = obj as GridSpanKey; + return (sk != null + && sk._start == _start + && sk._count == _count + && sk._u == _u); + } + + /// + /// Returns start index of the span. + /// + internal int Start { get { return (_start); } } + + /// + /// Returns span count. + /// + internal int Count { get { return (_count); } } + + /// + /// Returns true if this is a column span. + /// false if this is a row span. + /// + internal bool U { get { return (_u); } } + + private int _start; + private int _count; + private bool _u; + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/Grid/GridSplitter.cs similarity index 100% rename from src/Avalonia.Controls/GridSplitter.cs rename to src/Avalonia.Controls/Grid/GridSplitter.cs diff --git a/src/Avalonia.Controls/Grid/LayoutTimeSizeType.cs b/src/Avalonia.Controls/Grid/LayoutTimeSizeType.cs new file mode 100644 index 0000000000..1432c29ae5 --- /dev/null +++ b/src/Avalonia.Controls/Grid/LayoutTimeSizeType.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Avalonia.Controls +{ + /// + /// LayoutTimeSizeType is used internally and reflects layout-time size type. + /// + [System.Flags] + internal enum LayoutTimeSizeType : byte + { + None = 0x00, + Pixel = 0x01, + Auto = 0x02, + Star = 0x04, + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/MaxRatioComparer.cs b/src/Avalonia.Controls/Grid/MaxRatioComparer.cs new file mode 100644 index 0000000000..fe7eb356ec --- /dev/null +++ b/src/Avalonia.Controls/Grid/MaxRatioComparer.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Avalonia.Controls +{ + + /// + /// MaxRatioComparer. + /// Sort by w/max (stored in SizeCache), ascending. + /// We query the list from the back, i.e. in descending order of w/max. + /// + internal class MaxRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!Grid.CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/MaxRatioIndexComparer.cs b/src/Avalonia.Controls/Grid/MaxRatioIndexComparer.cs new file mode 100644 index 0000000000..01bcf85b27 --- /dev/null +++ b/src/Avalonia.Controls/Grid/MaxRatioIndexComparer.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; + +namespace Avalonia.Controls +{ + internal class MaxRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MaxRatioIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!Grid.CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/MinRatioComparer.cs b/src/Avalonia.Controls/Grid/MinRatioComparer.cs new file mode 100644 index 0000000000..8e0fa0a282 --- /dev/null +++ b/src/Avalonia.Controls/Grid/MinRatioComparer.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Avalonia.Controls +{ + /// + /// MinRatioComparer. + /// Sort by w/min (stored in MeasureSize), descending. + /// We query the list from the back, i.e. in ascending order of w/min. + /// + internal class MinRatioComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!Grid.CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/MinRatioIndexComparer.cs b/src/Avalonia.Controls/Grid/MinRatioIndexComparer.cs new file mode 100644 index 0000000000..01add324c1 --- /dev/null +++ b/src/Avalonia.Controls/Grid/MinRatioIndexComparer.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; + +namespace Avalonia.Controls +{ + internal class MinRatioIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal MinRatioIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!Grid.CompareNullRefs(definitionY, definitionX, out result)) + { + result = definitionY.MeasureSize.CompareTo(definitionX.MeasureSize); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/RoundingErrorIndexComparer.cs b/src/Avalonia.Controls/Grid/RoundingErrorIndexComparer.cs new file mode 100644 index 0000000000..a0a9035384 --- /dev/null +++ b/src/Avalonia.Controls/Grid/RoundingErrorIndexComparer.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; + +namespace Avalonia.Controls +{ + internal class RoundingErrorIndexComparer : IComparer + { + private readonly double[] errors; + + internal RoundingErrorIndexComparer(double[] errors) + { + Contract.Requires(errors != null); + this.errors = errors; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + int result; + + if (!Grid.CompareNullRefs(indexX, indexY, out result)) + { + double errorX = errors[indexX.Value]; + double errorY = errors[indexY.Value]; + result = errorX.CompareTo(errorY); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/SpanMaxDistributionOrderComparer.cs b/src/Avalonia.Controls/Grid/SpanMaxDistributionOrderComparer.cs new file mode 100644 index 0000000000..f6fbf4d2bb --- /dev/null +++ b/src/Avalonia.Controls/Grid/SpanMaxDistributionOrderComparer.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Avalonia.Controls +{ + internal class SpanMaxDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!Grid.CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + else + { + result = +1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = -1; + } + else + { + result = definitionX.SizeCache.CompareTo(definitionY.SizeCache); + } + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/SpanPreferredDistributionOrderComparer.cs b/src/Avalonia.Controls/Grid/SpanPreferredDistributionOrderComparer.cs new file mode 100644 index 0000000000..1adb62590c --- /dev/null +++ b/src/Avalonia.Controls/Grid/SpanPreferredDistributionOrderComparer.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Avalonia.Controls +{ + internal class SpanPreferredDistributionOrderComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!Grid.CompareNullRefs(definitionX, definitionY, out result)) + { + if (definitionX.UserSize.IsAuto) + { + if (definitionY.UserSize.IsAuto) + { + result = definitionX.MinSize.CompareTo(definitionY.MinSize); + } + else + { + result = -1; + } + } + else + { + if (definitionY.UserSize.IsAuto) + { + result = +1; + } + else + { + result = definitionX.PreferredSize.CompareTo(definitionY.PreferredSize); + } + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/StarWeightComparer.cs b/src/Avalonia.Controls/Grid/StarWeightComparer.cs new file mode 100644 index 0000000000..216f97f2c1 --- /dev/null +++ b/src/Avalonia.Controls/Grid/StarWeightComparer.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Avalonia.Controls +{ + /// + /// StarWeightComparer. + /// Sort by *-weight (stored in MeasureSize), ascending. + /// + internal class StarWeightComparer : IComparer + { + public int Compare(object x, object y) + { + DefinitionBase definitionX = x as DefinitionBase; + DefinitionBase definitionY = y as DefinitionBase; + + int result; + + if (!Grid.CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/StarWeightIndexComparer.cs b/src/Avalonia.Controls/Grid/StarWeightIndexComparer.cs new file mode 100644 index 0000000000..da5148e9a5 --- /dev/null +++ b/src/Avalonia.Controls/Grid/StarWeightIndexComparer.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; + +namespace Avalonia.Controls +{ + + internal class StarWeightIndexComparer : IComparer + { + private readonly DefinitionBase[] definitions; + + internal StarWeightIndexComparer(DefinitionBase[] definitions) + { + Contract.Requires(definitions != null); + this.definitions = definitions; + } + + public int Compare(object x, object y) + { + int? indexX = x as int?; + int? indexY = y as int?; + + DefinitionBase definitionX = null; + DefinitionBase definitionY = null; + + if (indexX != null) + { + definitionX = definitions[indexX.Value]; + } + if (indexY != null) + { + definitionY = definitions[indexY.Value]; + } + + int result; + + if (!Grid.CompareNullRefs(definitionX, definitionY, out result)) + { + result = definitionX.MeasureSize.CompareTo(definitionY.MeasureSize); + } + + return result; + } + } +} \ No newline at end of file From 34d2258aef257b5dcbaab0490138cab400e6dd29 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 21:00:50 +0800 Subject: [PATCH 057/179] Re-run CI --- src/Avalonia.Controls/Grid/Grid.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index a14e3f0d01..9291426c7f 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -21,6 +21,7 @@ namespace Avalonia.Controls { public class Grid : Panel { + internal bool CellsStructureDirty = true; internal bool SizeToContentU; internal bool SizeToContentV; From 5b9755329fbb92d5336bae80e1f101d60cc7db6c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 27 May 2019 21:08:49 +0800 Subject: [PATCH 058/179] Fix GridLineRenderer's DashStyles. --- src/Avalonia.Controls/Grid/GridLinesRenderer.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Grid/GridLinesRenderer.cs b/src/Avalonia.Controls/Grid/GridLinesRenderer.cs index 338b8c40c1..0f7f5963d2 100644 --- a/src/Avalonia.Controls/Grid/GridLinesRenderer.cs +++ b/src/Avalonia.Controls/Grid/GridLinesRenderer.cs @@ -14,19 +14,15 @@ namespace Avalonia.Controls /// static GridLinesRenderer() { - var oddDashArray = new List(); - oddDashArray.Add(_dashLength); - oddDashArray.Add(_dashLength); - var ds1 = new DashStyle(oddDashArray, 0); + var dashArray = new List() { _dashLength, _dashLength }; + + var ds1 = new DashStyle(dashArray, 0); _oddDashPen = new Pen(Brushes.Blue, _penWidth, lineCap: PenLineCap.Flat, dashStyle: ds1); - var evenDashArray = new List(); - evenDashArray.Add(_dashLength); - evenDashArray.Add(_dashLength); - var ds2 = new DashStyle(evenDashArray, 0); + var ds2 = new DashStyle(dashArray, _dashLength); _evenDashPen = new Pen(Brushes.Yellow, _penWidth, lineCap: PenLineCap.Flat, From 5b62d7c859bf73fc7a8db87aa06b3d0282c257b1 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 May 2019 00:11:42 +0800 Subject: [PATCH 059/179] More reorganizing + refactoring... --- .../{ => Grid}/ColumnDefinition.cs | 0 .../{ => Grid}/ColumnDefinitions.cs | 0 src/Avalonia.Controls/Grid/Grid.cs | 453 ++++++++---------- .../{ => Grid}/RowDefinition.cs | 0 .../{ => Grid}/RowDefinitions.cs | 0 5 files changed, 207 insertions(+), 246 deletions(-) rename src/Avalonia.Controls/{ => Grid}/ColumnDefinition.cs (100%) rename src/Avalonia.Controls/{ => Grid}/ColumnDefinitions.cs (100%) rename src/Avalonia.Controls/{ => Grid}/RowDefinition.cs (100%) rename src/Avalonia.Controls/{ => Grid}/RowDefinitions.cs (100%) diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/Grid/ColumnDefinition.cs similarity index 100% rename from src/Avalonia.Controls/ColumnDefinition.cs rename to src/Avalonia.Controls/Grid/ColumnDefinition.cs diff --git a/src/Avalonia.Controls/ColumnDefinitions.cs b/src/Avalonia.Controls/Grid/ColumnDefinitions.cs similarity index 100% rename from src/Avalonia.Controls/ColumnDefinitions.cs rename to src/Avalonia.Controls/Grid/ColumnDefinitions.cs diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index 9291426c7f..5def043136 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -21,31 +21,58 @@ namespace Avalonia.Controls { public class Grid : Panel { - internal bool CellsStructureDirty = true; internal bool SizeToContentU; internal bool SizeToContentV; internal bool HasStarCellsU; internal bool HasStarCellsV; internal bool HasGroup3CellsInAutoRows; - internal bool ColumnDefinitionsDirty = true; - internal bool RowDefinitionsDirty = true; - - // index of the first cell in first cell group + internal bool DefinitionsDirty; + internal bool IsTrivialGrid => (_definitionsU?.Length <= 1) && + (_definitionsV?.Length <= 1); internal int CellGroup1; - - // index of the first cell in second cell group internal int CellGroup2; - - // index of the first cell in third cell group internal int CellGroup3; - - // index of the first cell in fourth cell group internal int CellGroup4; + /// + /// Helper for Comparer methods. + /// + /// + /// true if one or both of x and y are null, in which case result holds + /// the relative sort order. + /// + internal 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); + } + // temporary array used during layout for various purposes // TempDefinitions.Length == Max(DefinitionsU.Length, DefinitionsV.Length) private DefinitionBase[] _tempDefinitions; + + private GridLinesRenderer _gridLinesRenderer; // Keeps track of definition indices. @@ -55,8 +82,10 @@ namespace Avalonia.Controls // Stores unrounded values and rounding errors during layout rounding. private double[] _roundingErrors; - private DefinitionBase[] _definitionsU; - private DefinitionBase[] _definitionsV; + private ColumnDefinitions _columnDefinitions; + private RowDefinitions _rowDefinitions; + private DefinitionBase[] _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + private DefinitionBase[] _definitionsV = new DefinitionBase[1] { new RowDefinition() }; // 5 is an arbitrary constant chosen to end the measure loop private const int _layoutLoopMaxCount = 5; @@ -67,6 +96,78 @@ namespace Avalonia.Controls private static readonly IComparer _maxRatioComparer; private static readonly IComparer _starWeightComparer; + /// + /// Helper accessor to layout time array of definitions. + /// + private DefinitionBase[] TempDefinitions + { + get + { + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; + + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) + { + WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); + if (tempDefinitionsWeakRef == null) + { + _tempDefinitions = new DefinitionBase[requiredLength]; + Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); + } + else + { + _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; + if (_tempDefinitions == null + || _tempDefinitions.Length < requiredLength) + { + _tempDefinitions = new DefinitionBase[requiredLength]; + tempDefinitionsWeakRef.Target = _tempDefinitions; + } + } + } + return (_tempDefinitions); + } + } + + /// + /// Helper accessor to definition indices. + /// + private int[] DefinitionIndices + { + get + { + int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; + + if (_definitionIndices == null || _definitionIndices.Length < requiredLength) + { + _definitionIndices = new int[requiredLength]; + } + + return _definitionIndices; + } + } + + /// + /// Helper accessor to rounding errors. + /// + private double[] RoundingErrors + { + get + { + int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); + + if (_roundingErrors == null && requiredLength == 0) + { + _roundingErrors = new double[1]; + } + else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) + { + _roundingErrors = new double[requiredLength]; + } + return _roundingErrors; + } + } + static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); @@ -128,6 +229,86 @@ namespace Avalonia.Controls set { SetValue(ShowGridLinesProperty, value); } } + /// + /// Gets or sets the columns definitions for the grid. + /// + public ColumnDefinitions ColumnDefinitions + { + get + { + if (_columnDefinitions == null) + { + ColumnDefinitions = new ColumnDefinitions(); + } + + return _columnDefinitions; + } + set + { + _columnDefinitions = value; + _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); + DefinitionsDirty = true; + + if (_columnDefinitions.Count > 0) + _definitionsU = _columnDefinitions.Cast().ToArray(); + + _columnDefinitions.CollectionChanged += delegate + { + if (_columnDefinitions.Count == 0) + { + _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; + } + else + { + _definitionsU = _columnDefinitions.Cast().ToArray(); + DefinitionsDirty = true; + } + Invalidate(); + }; + } + } + + /// + /// Gets or sets the row definitions for the grid. + /// + public RowDefinitions RowDefinitions + { + get + { + if (_rowDefinitions == null) + { + RowDefinitions = new RowDefinitions(); + } + + return _rowDefinitions; + } + set + { + _rowDefinitions = value; + _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); + + DefinitionsDirty = true; + + if (_rowDefinitions.Count > 0) + _definitionsV = _rowDefinitions.Cast().ToArray(); + + _rowDefinitions.CollectionChanged += delegate + { + if (_rowDefinitions.Count == 0) + { + _definitionsV = new DefinitionBase[1] { new RowDefinition() }; + } + else + { + _definitionsV = _rowDefinitions.Cast().ToArray(); + DefinitionsDirty = true; + } + Invalidate(); + }; + } + } + + /// /// Gets the value of the Column attached property for a control. /// @@ -218,95 +399,6 @@ namespace Avalonia.Controls element.SetValue(RowSpanProperty, value); } - private ColumnDefinitions _columnDefinitions; - private RowDefinitions _rowDefinitions; - - /// - /// Gets or sets the columns definitions for the grid. - /// - public ColumnDefinitions ColumnDefinitions - { - get - { - if (_columnDefinitions == null) - { - ColumnDefinitions = new ColumnDefinitions(); - } - - return _columnDefinitions; - } - set - { - _columnDefinitions = value; - _columnDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - ColumnDefinitionsDirty = true; - - if (_columnDefinitions.Count > 0) - _definitionsU = _columnDefinitions.Cast().ToArray(); - else - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - - _columnDefinitions.CollectionChanged += (_, e) => - { - if (_columnDefinitions.Count == 0) - { - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - } - else - { - _definitionsU = _columnDefinitions.Cast().ToArray(); - ColumnDefinitionsDirty = true; - } - Invalidate(); - }; - } - } - - /// - /// Gets or sets the row definitions for the grid. - /// - public RowDefinitions RowDefinitions - { - get - { - if (_rowDefinitions == null) - { - RowDefinitions = new RowDefinitions(); - } - - return _rowDefinitions; - } - set - { - _rowDefinitions = value; - _rowDefinitions.TrackItemPropertyChanged(_ => Invalidate()); - - RowDefinitionsDirty = true; - - if (_rowDefinitions.Count > 0) - _definitionsV = _rowDefinitions.Cast().ToArray(); - else - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - - _rowDefinitions.CollectionChanged += (_, e) => - { - if (_rowDefinitions.Count == 0) - { - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - } - else - { - _definitionsV = _rowDefinitions.Cast().ToArray(); - RowDefinitionsDirty = true; - } - Invalidate(); - }; - } - } - - internal bool IsTrivialGrid => (_definitionsU?.Length <= 1) && - (_definitionsV?.Length <= 1); - /// /// Content measurement. /// @@ -341,7 +433,7 @@ namespace Avalonia.Controls bool sizeToContentV = double.IsPositiveInfinity(constraint.Height); // Clear index information and rounding errors - if (RowDefinitionsDirty || ColumnDefinitionsDirty) + if (DefinitionsDirty) { if (_definitionIndices != null) { @@ -357,12 +449,11 @@ namespace Avalonia.Controls _roundingErrors = null; } } + + DefinitionsDirty = false; } - ValidateColumnDefinitionsStructure(); ValidateDefinitionsLayout(_definitionsU, sizeToContentU); - - ValidateRowDefinitionsStructure(); ValidateDefinitionsLayout(_definitionsV, sizeToContentV); CellsStructureDirty |= (SizeToContentU != sizeToContentU) @@ -453,27 +544,6 @@ namespace Avalonia.Controls return (gridDesiredSize); } - private void ValidateColumnDefinitionsStructure() - { - if (ColumnDefinitionsDirty) - { - if (_definitionsU == null) - _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; - ColumnDefinitionsDirty = false; - } - } - - private void ValidateRowDefinitionsStructure() - { - if (RowDefinitionsDirty) - { - if (_definitionsV == null) - _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - - RowDefinitionsDirty = false; - } - } - /// /// Content arrangement. /// @@ -554,12 +624,12 @@ namespace Avalonia.Controls /// /// Used from public ColumnDefinition ActualWidth. Calculates final width using offset data. /// - internal double GetFinalColumnDefinitionWidth(int columnIndex) + private double GetFinalColumnDefinitionWidth(int columnIndex) { double value = 0.0; // actual value calculations require structure to be up-to-date - if (!ColumnDefinitionsDirty) + if (!DefinitionsDirty) { value = _definitionsU[(columnIndex + 1) % _definitionsU.Length].FinalOffset; if (columnIndex != 0) { value -= _definitionsU[columnIndex].FinalOffset; } @@ -573,12 +643,12 @@ namespace Avalonia.Controls /// /// Used from public RowDefinition ActualHeight. Calculates final height using offset data. /// - internal double GetFinalRowDefinitionHeight(int rowIndex) + private double GetFinalRowDefinitionHeight(int rowIndex) { double value = 0.0; // actual value calculations require structure to be up-to-date - if (!RowDefinitionsDirty) + if (!DefinitionsDirty) { value = _definitionsV[(rowIndex + 1) % _definitionsV.Length].FinalOffset; if (rowIndex != 0) { value -= _definitionsV[rowIndex].FinalOffset; } @@ -589,7 +659,7 @@ namespace Avalonia.Controls /// /// Invalidates grid caches and makes the grid dirty for measure. /// - internal void Invalidate() + private void Invalidate() { CellsStructureDirty = true; InvalidateMeasure(); @@ -2111,14 +2181,11 @@ namespace Avalonia.Controls /// private void SetValid() { - if (IsTrivialGrid) + if (IsTrivialGrid && _tempDefinitions != null) { - if (_tempDefinitions != null) - { - // TempDefinitions has to be cleared to avoid "memory leaks" - Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); - _tempDefinitions = null; - } + // TempDefinitions has to be cleared to avoid "memory leaks" + Array.Clear(_tempDefinitions, 0, Math.Max(_definitionsU.Length, _definitionsV.Length)); + _tempDefinitions = null; } } @@ -2171,7 +2238,6 @@ namespace Avalonia.Controls return newValue; } - private static int ValidateColumn(AvaloniaObject o, int value) { if (value < 0) @@ -2200,115 +2266,10 @@ namespace Avalonia.Controls } } - /// - /// Helper for Comparer methods. - /// - /// - /// true if one or both of x and y are null, in which case result holds - /// the relative sort order. - /// - internal static bool CompareNullRefs(object x, object y, out int result) - { - result = 2; - - if (x == null) - { - if (y == null) - { - result = 0; - } - else - { - result = -1; - } - } - else - { - if (y == null) - { - result = 1; - } - } - - return (result != 2); - } - - /// - /// Helper accessor to layout time array of definitions. - /// - private DefinitionBase[] TempDefinitions - { - get - { - int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length) * 2; - - if (_tempDefinitions == null - || _tempDefinitions.Length < requiredLength) - { - WeakReference tempDefinitionsWeakRef = (WeakReference)Thread.GetData(_tempDefinitionsDataSlot); - if (tempDefinitionsWeakRef == null) - { - _tempDefinitions = new DefinitionBase[requiredLength]; - Thread.SetData(_tempDefinitionsDataSlot, new WeakReference(_tempDefinitions)); - } - else - { - _tempDefinitions = (DefinitionBase[])tempDefinitionsWeakRef.Target; - if (_tempDefinitions == null - || _tempDefinitions.Length < requiredLength) - { - _tempDefinitions = new DefinitionBase[requiredLength]; - tempDefinitionsWeakRef.Target = _tempDefinitions; - } - } - } - return (_tempDefinitions); - } - } - - /// - /// Helper accessor to definition indices. - /// - private int[] DefinitionIndices - { - get - { - int requiredLength = Math.Max(Math.Max(_definitionsU.Length, _definitionsV.Length), 1) * 2; - - if (_definitionIndices == null || _definitionIndices.Length < requiredLength) - { - _definitionIndices = new int[requiredLength]; - } - - return _definitionIndices; - } - } - - /// - /// Helper accessor to rounding errors. - /// - private double[] RoundingErrors - { - get - { - int requiredLength = Math.Max(_definitionsU.Length, _definitionsV.Length); - - if (_roundingErrors == null && requiredLength == 0) - { - _roundingErrors = new double[1]; - } - else if (_roundingErrors == null || _roundingErrors.Length < requiredLength) - { - _roundingErrors = new double[requiredLength]; - } - return _roundingErrors; - } - } - /// /// Returns *-weight, adjusted for scale computed during Phase 1 /// - static double StarWeight(DefinitionBase def, double scale) + private static double StarWeight(DefinitionBase def, double scale) { if (scale < 0.0) { diff --git a/src/Avalonia.Controls/RowDefinition.cs b/src/Avalonia.Controls/Grid/RowDefinition.cs similarity index 100% rename from src/Avalonia.Controls/RowDefinition.cs rename to src/Avalonia.Controls/Grid/RowDefinition.cs diff --git a/src/Avalonia.Controls/RowDefinitions.cs b/src/Avalonia.Controls/Grid/RowDefinitions.cs similarity index 100% rename from src/Avalonia.Controls/RowDefinitions.cs rename to src/Avalonia.Controls/Grid/RowDefinitions.cs From 6dba3467b77773570466bc5089e6998172bdc016 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 May 2019 00:28:52 +0800 Subject: [PATCH 060/179] Revert `ShowGridLines` default value. --- src/Avalonia.Controls/Grid/Grid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index 5def043136..449b92ac52 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -218,7 +218,7 @@ namespace Avalonia.Controls public static readonly StyledProperty ShowGridLinesProperty = AvaloniaProperty.Register( nameof(ShowGridLines), - defaultValue: true); + defaultValue: false); /// /// ShowGridLines property. From ad45848a294a3ba7af3b98bcf104e9c802d20994 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 May 2019 16:35:45 +0800 Subject: [PATCH 061/179] Implement and make SharedSizeScopes work :) --- .../Grid/ColumnDefinition.cs | 6 +- src/Avalonia.Controls/Grid/DefinitionBase.cs | 286 ++++++++++++++++-- src/Avalonia.Controls/Grid/Grid.cs | 114 +++++-- src/Avalonia.Controls/Grid/RowDefinition.cs | 6 +- src/Avalonia.Controls/Grid/SharedSizeScope.cs | 43 +++ src/Avalonia.Controls/Grid/SharedSizeState.cs | 209 +++++++++++++ 6 files changed, 606 insertions(+), 58 deletions(-) create mode 100644 src/Avalonia.Controls/Grid/SharedSizeScope.cs create mode 100644 src/Avalonia.Controls/Grid/SharedSizeState.cs diff --git a/src/Avalonia.Controls/Grid/ColumnDefinition.cs b/src/Avalonia.Controls/Grid/ColumnDefinition.cs index 56a6d41b7b..015484dbcc 100644 --- a/src/Avalonia.Controls/Grid/ColumnDefinition.cs +++ b/src/Avalonia.Controls/Grid/ColumnDefinition.cs @@ -88,10 +88,10 @@ namespace Avalonia.Controls set { SetValue(WidthProperty, value); } } - internal override GridLength UserSize => this.Width; + internal override GridLength UserSizeValueCache => this.Width; - internal override double UserMinSize => this.MinWidth; + internal override double UserMinSizeValueCache => this.MinWidth; - internal override double UserMaxSize => this.MaxWidth; + internal override double UserMaxSizeValueCache => this.MaxWidth; } } diff --git a/src/Avalonia.Controls/Grid/DefinitionBase.cs b/src/Avalonia.Controls/Grid/DefinitionBase.cs index ad2f33a18a..e21d55c1f6 100644 --- a/src/Avalonia.Controls/Grid/DefinitionBase.cs +++ b/src/Avalonia.Controls/Grid/DefinitionBase.cs @@ -3,9 +3,7 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using Avalonia.Utilities; namespace Avalonia.Controls { @@ -14,6 +12,26 @@ namespace Avalonia.Controls /// public abstract class DefinitionBase : AvaloniaObject { + /// + /// Static ctor. Used for static registration of properties. + /// + static DefinitionBase() + { + SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + } + internal bool UseSharedMinimum { get; set; } + internal bool LayoutWasUpdated { get; set; } + + private int _parentIndex = -1; // this instance's index in parent's children collection + private 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) + internal SharedSizeScope _privateSharedSizeScope; + private SharedSizeState _sharedState; // reference to shared state object this instance is registered with + private bool _successUpdateSharedScope; + /// /// Defines the property. /// @@ -28,31 +46,182 @@ namespace Avalonia.Controls get { return GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } + /// + /// Callback to notify about entering model tree. + /// + internal void OnEnterParentTree(Grid grid, int index) + { + Parent = grid; + _parentIndex = index; + } + + internal void UpdateSharedScope() + { + if (_sharedState == null & + SharedSizeGroup != null & + Parent?.sharedSizeScope != null & + !_successUpdateSharedScope) + { + _privateSharedSizeScope = Parent.sharedSizeScope; + _sharedState = _privateSharedSizeScope.EnsureSharedState(SharedSizeGroup); + _sharedState.AddMember(this); + _successUpdateSharedScope = true; + } + } + + internal Grid Parent { get; set; } /// - /// Internal helper to access up-to-date UserSize property value. + /// Callback to notify about exitting model tree. /// - internal abstract GridLength UserSize { get; } + internal void OnExitParentTree() + { + _offset = 0; + if (_sharedState != null) + { + _sharedState.RemoveMember(this); + _sharedState = null; + } + } /// - /// Internal helper to access up-to-date UserMinSize property value. + /// Performs action preparing definition to enter layout calculation mode. /// - internal abstract double UserMinSize { get; } + internal void OnBeforeLayout(Grid grid) + { + if (SharedSizeGroup != null) + UpdateSharedScope(); + + // reset layout state. + _minSize = 0; + LayoutWasUpdated = true; + + // defer verification for shared definitions + if (_sharedState != null) + { + _sharedState.EnsureDeferredValidation(grid); + } + } /// - /// Internal helper to access up-to-date UserMaxSize property value. + /// 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) + { + _sharedState?.Invalidate(); + } + + /// + /// 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(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) + { + 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); + } + + /// + /// 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 abstract double UserMaxSize { get; } + 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); + _parentIndex = value; + } + } /// /// Layout-time user size type. /// - internal LayoutTimeSizeType SizeType { get; set; } - + internal LayoutTimeSizeType SizeType + { + get { return (_sizeType); } + set { _sizeType = value; } + } + /// /// Returns or sets measure size for the definition. /// - internal double MeasureSize { get; set; } + internal double MeasureSize + { + get { return (_measureSize); } + set { _measureSize = value; } + } /// /// Returns definition's layout time type sensitive preferred size. @@ -65,37 +234,108 @@ namespace Avalonia.Controls get { double preferredSize = MinSize; - if (SizeType != LayoutTimeSizeType.Auto - && preferredSize < MeasureSize) + if (_sizeType != LayoutTimeSizeType.Auto + && preferredSize < _measureSize) { - preferredSize = MeasureSize; + preferredSize = _measureSize; } - return (preferredSize); + return (preferredSize); } } /// /// Returns or sets size cache for the definition. /// - internal double SizeCache { get; set; } + internal double SizeCache + { + get { return (_sizeCache); } + set { _sizeCache = value; } + } /// - /// Used during measure and arrange to accumulate size for "Auto" and "Star" DefinitionBase's + /// Returns min size. /// - internal double MinSize { get; set; } + internal double MinSize + { + get + { + double minSize = _minSize; + if (UseSharedMinimum + && _sharedState != null + && minSize < _sharedState.MinSize) + { + minSize = _sharedState.MinSize; + } + return (minSize); + } + } /// - /// Updates min size. + /// Returns min size, always taking into account shared state. /// - /// New size. - internal void UpdateMinSize(double minSize) + internal double MinSizeForArrange { - MinSize = Math.Max(MinSize, minSize); + get + { + double minSize = _minSize; + if (_sharedState != null + && (UseSharedMinimum || !LayoutWasUpdated) + && minSize < _sharedState.MinSize) + { + minSize = _sharedState.MinSize; + } + return (minSize); + } } /// /// Offset. /// - internal double FinalOffset { get; set; } + 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; } + + private static void OnSharedSizeGroupPropertyChanged(DefinitionBase definition, AvaloniaPropertyChangedEventArgs e) + { + 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)) + { + var 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); + + } + } + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index 449b92ac52..c08c4396c5 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -86,6 +86,7 @@ namespace Avalonia.Controls private RowDefinitions _rowDefinitions; private DefinitionBase[] _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; private DefinitionBase[] _definitionsV = new DefinitionBase[1] { new RowDefinition() }; + internal SharedSizeScope sharedSizeScope; // 5 is an arbitrary constant chosen to end the measure loop private const int _layoutLoopMaxCount = 5; @@ -171,6 +172,9 @@ namespace Avalonia.Controls static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopePropertyChanged); + BoundsProperty.Changed.AddClassHandler(BoundsPropertyChanged); + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); _tempDefinitionsDataSlot = Thread.AllocateDataSlot(); @@ -181,6 +185,26 @@ namespace Avalonia.Controls _starWeightComparer = new StarWeightComparer(); } + private static void BoundsPropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs arg2) + { + for (int i = 0; i < grid._definitionsU.Length; i++) + grid._definitionsU[i].OnUserSizePropertyChanged(arg2); + for (int i = 0; i < grid._definitionsV.Length; i++) + grid._definitionsV[i].OnUserSizePropertyChanged(arg2); + } + + private static void IsSharedSizeScopePropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) + { + if ((bool)e.NewValue) + { + grid.sharedSizeScope = new SharedSizeScope(); + } + else + { + grid.sharedSizeScope = null; + } + } + /// /// Defines the Column attached property. /// @@ -252,8 +276,12 @@ namespace Avalonia.Controls if (_columnDefinitions.Count > 0) _definitionsU = _columnDefinitions.Cast().ToArray(); + CallEnterParentTree(_definitionsU); + _columnDefinitions.CollectionChanged += delegate { + CallExitParentTree(_definitionsU); + if (_columnDefinitions.Count == 0) { _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; @@ -263,11 +291,26 @@ namespace Avalonia.Controls _definitionsU = _columnDefinitions.Cast().ToArray(); DefinitionsDirty = true; } + + CallEnterParentTree(_definitionsU); + Invalidate(); }; } } + private void CallEnterParentTree(DefinitionBase[] definitionsU) + { + for (int i = 0; i < definitionsU.Length; i++) + definitionsU[i].OnEnterParentTree(this, i); + } + + private void CallExitParentTree(DefinitionBase[] definitionsU) + { + for (int i = 0; i < definitionsU.Length; i++) + definitionsU[i].OnExitParentTree(); + } + /// /// Gets or sets the row definitions for the grid. /// @@ -294,6 +337,8 @@ namespace Avalonia.Controls _rowDefinitions.CollectionChanged += delegate { + CallExitParentTree(_definitionsU); + if (_rowDefinitions.Count == 0) { _definitionsV = new DefinitionBase[1] { new RowDefinition() }; @@ -303,6 +348,8 @@ namespace Avalonia.Controls _definitionsV = _rowDefinitions.Cast().ToArray(); DefinitionsDirty = true; } + CallEnterParentTree(_definitionsU); + Invalidate(); }; } @@ -359,6 +406,16 @@ namespace Avalonia.Controls return element.GetValue(IsSharedSizeScopeProperty); } + /// + /// Sets the value of the IsSharedSizeScope attached property for a control. + /// + /// The control. + /// The control's IsSharedSizeScope value. + public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) + { + element.SetValue(IsSharedSizeScopeProperty, value); + } + /// /// Sets the value of the Column attached property for a control. /// @@ -659,7 +716,7 @@ namespace Avalonia.Controls /// /// Invalidates grid caches and makes the grid dirty for measure. /// - private void Invalidate() + internal void Invalidate() { CellsStructureDirty = true; InvalidateMeasure(); @@ -780,8 +837,7 @@ namespace Avalonia.Controls { for (int i = 0; i < definitions.Length; ++i) { - // Reset minimum size. - definitions[i].MinSize = 0; + definitions[i].OnBeforeLayout(this); double userMinSize = definitions[i].UserMinSize; double userMaxSize = definitions[i].UserMaxSize; @@ -858,11 +914,11 @@ namespace Avalonia.Controls { if (isRows) { - _definitionsV[i].MinSize = minSizes[i]; + _definitionsV[i].SetMinSize(minSizes[i]); } else { - _definitionsU[i].MinSize = minSizes[i]; + _definitionsU[i].SetMinSize(minSizes[i]); } } } @@ -1710,7 +1766,7 @@ namespace Avalonia.Controls if (def.UserSize.IsStar) { - // Debug.Assert(!def.IsShared, "*-defs cannot be shared"); + Debug.Assert(!def.IsShared, "*-defs cannot be shared"); if (def.MeasureSize < 0.0) { @@ -1721,14 +1777,14 @@ namespace Avalonia.Controls double starWeight = StarWeight(def, scale); totalStarWeight += starWeight; - if (def.MinSize > 0.0) + if (def.MinSizeForArrange > 0.0) { // store ratio w/min in MeasureSize (for now) definitionIndices[minCount++] = i; - def.MeasureSize = starWeight / def.MinSize; + def.MeasureSize = starWeight / def.MinSizeForArrange; } - double effectiveMaxSize = Math.Max(def.MinSize, def.UserMaxSize); + double effectiveMaxSize = Math.Max(def.MinSizeForArrange, def.UserMaxSize); if (!double.IsPositiveInfinity(effectiveMaxSize)) { // store ratio w/max in SizeCache (for now) @@ -1748,26 +1804,26 @@ namespace Avalonia.Controls break; case (GridUnitType.Auto): - userSize = def.MinSize; + 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.MinSize, Math.Min(userSize, 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; } } @@ -1831,14 +1887,14 @@ namespace Avalonia.Controls { resolvedIndex = definitionIndices[minCount - 1]; resolvedDef = definitions[resolvedIndex]; - resolvedSize = resolvedDef.MinSize; + resolvedSize = resolvedDef.MinSizeForArrange; --minCount; } else { resolvedIndex = definitionIndices[defCount + maxCount - 1]; resolvedDef = definitions[resolvedIndex]; - resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); + resolvedSize = Math.Max(resolvedDef.MinSizeForArrange, resolvedDef.UserMaxSize); --maxCount; } @@ -1961,7 +2017,7 @@ namespace Avalonia.Controls // 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); + 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; @@ -2053,7 +2109,7 @@ namespace Avalonia.Controls { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache - dpiIncrement; - final = Math.Max(final, definition.MinSize); + final = Math.Max(final, definition.MinSizeForArrange); if (final < definition.SizeCache) { adjustedSize -= dpiIncrement; @@ -2069,7 +2125,7 @@ namespace Avalonia.Controls { DefinitionBase definition = definitions[definitionIndices[i]]; double final = definition.SizeCache + dpiIncrement; - final = Math.Max(final, definition.MinSize); + final = Math.Max(final, definition.MinSizeForArrange); if (final > definition.SizeCache) { adjustedSize += dpiIncrement; diff --git a/src/Avalonia.Controls/Grid/RowDefinition.cs b/src/Avalonia.Controls/Grid/RowDefinition.cs index d9120b010e..1cb09e16e9 100644 --- a/src/Avalonia.Controls/Grid/RowDefinition.cs +++ b/src/Avalonia.Controls/Grid/RowDefinition.cs @@ -89,10 +89,10 @@ namespace Avalonia.Controls } - internal override GridLength UserSize => this.Height; + internal override GridLength UserSizeValueCache => this.Height; - internal override double UserMinSize => this.MinHeight; + internal override double UserMinSizeValueCache => this.MinHeight; - internal override double UserMaxSize => this.MaxHeight; + internal override double UserMaxSizeValueCache => this.MaxHeight; } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/SharedSizeScope.cs b/src/Avalonia.Controls/Grid/SharedSizeScope.cs new file mode 100644 index 0000000000..6835d13132 --- /dev/null +++ b/src/Avalonia.Controls/Grid/SharedSizeScope.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Diagnostics; + +namespace Avalonia.Controls +{ + /// + /// 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 + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Grid/SharedSizeState.cs b/src/Avalonia.Controls/Grid/SharedSizeState.cs new file mode 100644 index 0000000000..2b99c09861 --- /dev/null +++ b/src/Avalonia.Controls/Grid/SharedSizeState.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implementation of per shared group state object + /// + internal class SharedSizeState + { + 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 + + /// + /// 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; + } + } +} \ No newline at end of file From 2310dab605457d38fedbb720eccb76d3c0d62793 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 May 2019 17:06:22 +0800 Subject: [PATCH 062/179] Inherit SharedSize scopes to child grids. --- src/Avalonia.Controls/Grid/DefinitionBase.cs | 5 ++-- src/Avalonia.Controls/Grid/Grid.cs | 26 ++++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Grid/DefinitionBase.cs b/src/Avalonia.Controls/Grid/DefinitionBase.cs index e21d55c1f6..a59ab3cdb9 100644 --- a/src/Avalonia.Controls/Grid/DefinitionBase.cs +++ b/src/Avalonia.Controls/Grid/DefinitionBase.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls { SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); } + internal bool UseSharedMinimum { get; set; } internal bool LayoutWasUpdated { get; set; } @@ -59,10 +60,10 @@ namespace Avalonia.Controls { if (_sharedState == null & SharedSizeGroup != null & - Parent?.sharedSizeScope != null & + Parent?.PrivateSharedSizeScope != null & !_successUpdateSharedScope) { - _privateSharedSizeScope = Parent.sharedSizeScope; + _privateSharedSizeScope = Parent.PrivateSharedSizeScope; _sharedState = _privateSharedSizeScope.EnsureSharedState(SharedSizeGroup); _sharedState.AddMember(this); _successUpdateSharedScope = true; diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index c08c4396c5..075d2c098f 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -86,7 +86,12 @@ namespace Avalonia.Controls private RowDefinitions _rowDefinitions; private DefinitionBase[] _definitionsU = new DefinitionBase[1] { new ColumnDefinition() }; private DefinitionBase[] _definitionsV = new DefinitionBase[1] { new RowDefinition() }; - internal SharedSizeScope sharedSizeScope; + + internal SharedSizeScope PrivateSharedSizeScope + { + get { return GetPrivateSharedSizeScope(this); } + set { SetPrivateSharedSizeScope(this, value); } + } // 5 is an arbitrary constant chosen to end the measure loop private const int _layoutLoopMaxCount = 5; @@ -197,11 +202,11 @@ namespace Avalonia.Controls { if ((bool)e.NewValue) { - grid.sharedSizeScope = new SharedSizeScope(); + grid.PrivateSharedSizeScope = new SharedSizeScope(); } else { - grid.sharedSizeScope = null; + grid.PrivateSharedSizeScope = null; } } @@ -236,6 +241,9 @@ namespace Avalonia.Controls public static readonly AttachedProperty IsSharedSizeScopeProperty = AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + internal static readonly AttachedProperty PrivateSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached("PrivateSharedSizeScope", null, inherits: true); + /// /// Defines the property. /// @@ -409,13 +417,21 @@ namespace Avalonia.Controls /// /// Sets the value of the IsSharedSizeScope attached property for a control. /// - /// The control. - /// The control's IsSharedSizeScope value. public static void SetIsSharedSizeScope(AvaloniaObject element, bool value) { element.SetValue(IsSharedSizeScopeProperty, value); } + internal static SharedSizeScope GetPrivateSharedSizeScope(AvaloniaObject element) + { + return element.GetValue(PrivateSharedSizeScopeProperty); + } + + internal static void SetPrivateSharedSizeScope(AvaloniaObject element, SharedSizeScope value) + { + element.SetValue(PrivateSharedSizeScopeProperty, value); + } + /// /// Sets the value of the Column attached property for a control. /// From d97835ee624183e60113b25ea6b2d8e58872129b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 May 2019 21:46:14 +0800 Subject: [PATCH 063/179] Add SharedSizeScope tests from #1945 Fix IsSharedSizeScope/PrivateSharedSizeScope so that it can be set on non-`Grid` controls. --- src/Avalonia.Controls/Grid/Grid.cs | 24 +- .../Avalonia.Controls.UnitTests/GridTests.cs | 268 +++++++++++++++++- 2 files changed, 286 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index 075d2c098f..c45d7c2461 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -177,7 +177,7 @@ namespace Avalonia.Controls static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); - IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopePropertyChanged); + IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopePropertyChanged); BoundsProperty.Changed.AddClassHandler(BoundsPropertyChanged); AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); @@ -196,20 +196,32 @@ namespace Avalonia.Controls grid._definitionsU[i].OnUserSizePropertyChanged(arg2); for (int i = 0; i < grid._definitionsV.Length; i++) grid._definitionsV[i].OnUserSizePropertyChanged(arg2); + + UpdateSharedSizeScopes(grid); } - private static void IsSharedSizeScopePropertyChanged(Grid grid, AvaloniaPropertyChangedEventArgs e) + private static void IsSharedSizeScopePropertyChanged(Control control, AvaloniaPropertyChangedEventArgs e) { if ((bool)e.NewValue) { - grid.PrivateSharedSizeScope = new SharedSizeScope(); + control.SetValue(Grid.PrivateSharedSizeScopeProperty, new SharedSizeScope()); } else { - grid.PrivateSharedSizeScope = null; + control.SetValue(Grid.PrivateSharedSizeScopeProperty, null); } } + static void UpdateSharedSizeScopes(Grid grid) + { + for (int i = 0; i < grid._definitionsU.Length; i++) + if (grid._definitionsU[i].SharedSizeGroup != null) + grid._definitionsU[i].UpdateSharedScope(); + for (int i = 0; i < grid._definitionsV.Length; i++) + if (grid._definitionsV[i].SharedSizeGroup != null) + grid._definitionsV[i].UpdateSharedScope(); + } + /// /// Defines the Column attached property. /// @@ -242,7 +254,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); internal static readonly AttachedProperty PrivateSharedSizeScopeProperty = - AvaloniaProperty.RegisterAttached("PrivateSharedSizeScope", null, inherits: true); + AvaloniaProperty.RegisterAttached("&&PrivateSharedSizeScope", null, inherits: true); /// /// Defines the property. @@ -614,6 +626,7 @@ namespace Avalonia.Controls { } + UpdateSharedSizeScopes(this); return (gridDesiredSize); } @@ -688,6 +701,7 @@ namespace Avalonia.Controls RowDefinitions[i].ActualHeight = GetFinalRowDefinitionHeight(i); } + UpdateSharedSizeScopes(this); return (arrangeSize); } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 50335c44ee..7e47f60eb5 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1,6 +1,9 @@ // 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.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests @@ -179,5 +182,268 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } + + [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.PrivateSharedSizeScope != null)); + } + + [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.PrivateSharedSizeScope != null)); + } + + [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.PrivateSharedSizeScope != null)); + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + Assert.All(grids, g => Assert.False(g.PrivateSharedSizeScope != null)); + Assert.Equal(null, root.GetValue(Grid.PrivateSharedSizeScopeProperty)); + } + + [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.Child = grid; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + 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; + } } -} +} \ No newline at end of file From c70ffb564bc837d636c8c5f20ecc3bececf02719 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 May 2019 21:57:07 +0800 Subject: [PATCH 064/179] Restore unit tests part 2 --- src/Avalonia.Controls/Grid/DefinitionBase.cs | 5 +- src/Avalonia.Controls/Grid/Grid.cs | 5 + .../Avalonia.Controls.UnitTests/GridTests.cs | 263 --------- .../SharedSizeScopeTests.cs | 527 +++++++++--------- 4 files changed, 267 insertions(+), 533 deletions(-) diff --git a/src/Avalonia.Controls/Grid/DefinitionBase.cs b/src/Avalonia.Controls/Grid/DefinitionBase.cs index a59ab3cdb9..051ed49289 100644 --- a/src/Avalonia.Controls/Grid/DefinitionBase.cs +++ b/src/Avalonia.Controls/Grid/DefinitionBase.cs @@ -31,7 +31,6 @@ namespace Avalonia.Controls private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) internal SharedSizeScope _privateSharedSizeScope; private SharedSizeState _sharedState; // reference to shared state object this instance is registered with - private bool _successUpdateSharedScope; /// /// Defines the property. @@ -60,13 +59,11 @@ namespace Avalonia.Controls { if (_sharedState == null & SharedSizeGroup != null & - Parent?.PrivateSharedSizeScope != null & - !_successUpdateSharedScope) + Parent?.PrivateSharedSizeScope != null ) { _privateSharedSizeScope = Parent.PrivateSharedSizeScope; _sharedState = _privateSharedSizeScope.EnsureSharedState(SharedSizeGroup); _sharedState.AddMember(this); - _successUpdateSharedScope = true; } } diff --git a/src/Avalonia.Controls/Grid/Grid.cs b/src/Avalonia.Controls/Grid/Grid.cs index c45d7c2461..511853a982 100644 --- a/src/Avalonia.Controls/Grid/Grid.cs +++ b/src/Avalonia.Controls/Grid/Grid.cs @@ -35,6 +35,11 @@ namespace Avalonia.Controls internal int CellGroup3; internal int CellGroup4; + internal bool HasSharedSizeScope() + { + return this.GetValue(Grid.PrivateSharedSizeScopeProperty) != null; + } + /// /// Helper for Comparer methods. /// diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 7e47f60eb5..7126075b9e 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -182,268 +182,5 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsMeasureValid); } - - [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.PrivateSharedSizeScope != null)); - } - - [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.PrivateSharedSizeScope != null)); - } - - [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.PrivateSharedSizeScope != null)); - root.SetValue(Grid.IsSharedSizeScopeProperty, false); - Assert.All(grids, g => Assert.False(g.PrivateSharedSizeScope != null)); - Assert.Equal(null, root.GetValue(Grid.PrivateSharedSizeScopeProperty)); - } - - [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.Child = grid; - root.SetValue(Grid.IsSharedSizeScopeProperty, true); - 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; - } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs index 03eceb17f5..467c25bfc6 100644 --- a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -6,279 +6,274 @@ 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.PrivateSharedSizeScopeProperty)); + } + + [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 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; - // } + [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; + } } -} +} \ No newline at end of file From 34358e0a2635258f0531f1c43496ac3b59295ae9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 May 2019 20:18:24 +0100 Subject: [PATCH 065/179] dont raise gestures if the events were already handled. --- src/Avalonia.Input/Gestures.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 8a514ca685..23b0ad466e 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -38,7 +38,10 @@ namespace Avalonia.Input } else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) { - e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + if (!ev.Handled) + { + e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + } } } } @@ -51,7 +54,10 @@ namespace Avalonia.Input if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) { - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); + if (!ev.Handled) + { + ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent)); + } } } } From 1fd30a91c3733c7264f3bb65fc89cf2b307259c8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 May 2019 20:31:54 +0100 Subject: [PATCH 066/179] add a unit test for gestures not being raised when parent handles event. --- .../GestureTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs index 931c1b5268..bebf7f9bc9 100644 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs @@ -78,5 +78,36 @@ namespace Avalonia.Interactivity.UnitTests Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); } + + [Fact] + public void DoubleTapped_Should_Not_Be_Rasied_if_Pressed_is_Handled() + { + Border border = new Border(); + var decorator = new Decorator + { + Child = border + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => + { + result.Add("dp"); + e.Handled = true; + }); + + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp" }, result); + } } } From f556db57b439f8474d8ed36834a27577ea974442 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 May 2019 23:33:54 +0300 Subject: [PATCH 067/179] Refactor pointer events to support touch events --- .../Calendar/CalendarButton.cs | 13 -- .../Calendar/CalendarDayButton.cs | 13 -- .../Calendar/CalendarItem.cs | 32 +---- src/Avalonia.Controls/MenuItem.cs | 18 +-- src/Avalonia.Input/IMouseDevice.cs | 2 + src/Avalonia.Input/IPointer.cs | 17 +++ src/Avalonia.Input/IPointerDevice.cs | 2 - src/Avalonia.Input/MouseDevice.cs | 134 ++++++++++-------- src/Avalonia.Input/PointerEventArgs.cs | 98 ++++++++++--- src/Avalonia.Input/PointerPoint.cs | 45 ++++++ src/Avalonia.Input/PointerWheelEventArgs.cs | 11 ++ src/Avalonia.Input/Raw/RawMouseEventArgs.cs | 3 + src/Avalonia.Input/Raw/RawTouchEventArgs.cs | 15 ++ .../ButtonTests.cs | 106 +++++--------- .../ComboBoxTests.cs | 14 +- .../ContextMenuTests.cs | 43 ++---- .../ListBoxTests.cs | 9 +- .../ListBoxTests_Single.cs | 41 ++---- .../MouseTestHelper.cs | 114 +++++++++++++++ .../DefaultMenuInteractionHandlerTests.cs | 52 +++++-- .../Primitives/SelectingItemsControlTests.cs | 15 +- .../TreeViewTests.cs | 89 +++++------- .../Avalonia.Interactivity.UnitTests.csproj | 2 + .../GestureTests.cs | 14 +- .../Xaml/XamlIlTests.cs | 13 +- 25 files changed, 515 insertions(+), 400 deletions(-) create mode 100644 src/Avalonia.Input/IPointer.cs create mode 100644 src/Avalonia.Input/PointerPoint.cs create mode 100644 src/Avalonia.Input/Raw/RawTouchEventArgs.cs create mode 100644 tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 224d9b782b..53852defb3 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives if (e.MouseButton == MouseButton.Left) CalendarLeftMouseButtonUp?.Invoke(this, e); } - - /// - /// We need to simulate the MouseLeftButtonUp event for the - /// CalendarButton that stays in Pressed state after MouseCapture is - /// released since there is no actual MouseLeftButtonUp event for the - /// release. - /// - /// Event arguments. - internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) - { - e.Handled = false; - base.OnPointerReleased(e); - } } } diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 1b36f92fd3..cb2a98e5ca 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives if (e.MouseButton == MouseButton.Left) CalendarDayButtonMouseUp?.Invoke(this, e); } - - /// - /// We need to simulate the MouseLeftButtonUp event for the - /// CalendarDayButton that stays in Pressed state after MouseCapture is - /// released since there is no actual MouseLeftButtonUp event for the - /// release. - /// - /// Event arguments. - internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) - { - e.Handled = false; - base.OnPointerReleased(e); - } } } diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index fb6dacaf81..8232697c18 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -934,22 +934,6 @@ namespace Avalonia.Controls.Primitives // The button is in Pressed state. Change the state to normal. if (e.Device.Captured == b) e.Device.Capture(null); - // null check is added for unit tests - if (_downEventArg != null) - { - var arg = - new PointerReleasedEventArgs() - { - Device = _downEventArg.Device, - MouseButton = _downEventArg.MouseButton, - Handled = _downEventArg.Handled, - InputModifiers = _downEventArg.InputModifiers, - Route = _downEventArg.Route, - Source = _downEventArg.Source - }; - - b.SendMouseLeftButtonUp(arg); - } _lastCalendarDayButton = b; } } @@ -1221,21 +1205,7 @@ namespace Avalonia.Controls.Primitives if (e.Device.Captured == b) e.Device.Capture(null); //b.ReleaseMouseCapture(); - if (_downEventArgYearView != null) - { - var args = - new PointerReleasedEventArgs() - { - Device = _downEventArgYearView.Device, - MouseButton = _downEventArgYearView.MouseButton, - Handled = _downEventArgYearView.Handled, - InputModifiers = _downEventArgYearView.InputModifiers, - Route = _downEventArgYearView.Route, - Source = _downEventArgYearView.Source - }; - - b.SendMouseLeftButtonUp(args); - } + _lastCalendarButton = b; } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index d8473dc613..d0ab0a0c8b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -337,12 +337,9 @@ namespace Avalonia.Controls { base.OnPointerEnter(e); - RaiseEvent(new PointerEventArgs - { - Device = e.Device, - RoutedEvent = PointerEnterItemEvent, - Source = this, - }); + var point = e.GetPointerPoint(null); + RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position, + point.Properties, e.InputModifiers)); } /// @@ -350,12 +347,9 @@ namespace Avalonia.Controls { base.OnPointerLeave(e); - RaiseEvent(new PointerEventArgs - { - Device = e.Device, - RoutedEvent = PointerLeaveItemEvent, - Source = this, - }); + var point = e.GetPointerPoint(null); + RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position, + point.Properties, e.InputModifiers)); } /// diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index a1d1bb3eb8..76be8ba308 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -12,5 +12,7 @@ namespace Avalonia.Input /// Gets the mouse position, in screen coordinates. /// PixelPoint Position { get; } + + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/IPointer.cs b/src/Avalonia.Input/IPointer.cs new file mode 100644 index 0000000000..15f1fef531 --- /dev/null +++ b/src/Avalonia.Input/IPointer.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Input +{ + public interface IPointer + { + void Capture(IInputElement control); + IInputElement Captured { get; } + PointerType Type { get; } + bool IsPrimary { get; } + + } + + public enum PointerType + { + Mouse, + Touch + } +} diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index 932cbc989f..8613717f60 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -12,7 +12,5 @@ namespace Avalonia.Input void Capture(IInputElement control); Point GetPosition(IVisual relativeTo); - - void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index c195209305..352c69a726 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -14,14 +14,17 @@ namespace Avalonia.Input /// /// Represents a mouse device. /// - public class MouseDevice : IMouseDevice + public class MouseDevice : IMouseDevice, IPointer { private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; private IInputElement _captured; private IDisposable _capturedSubscription; - + + PointerType IPointer.Type => PointerType.Mouse; + bool IPointer.IsPrimary => true; + /// /// Gets the control that is currently capturing by the mouse, if any. /// @@ -117,6 +120,18 @@ namespace Avalonia.Input } } + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + return rv; + } + private void ProcessRawEvent(RawMouseEventArgs e) { Contract.Requires(e != null); @@ -124,7 +139,7 @@ namespace Avalonia.Input var mouse = (IMouseDevice)e.Device; Position = e.Root.PointToScreen(e.Position); - + var props = CreateProperties(e); switch (e.Type) { case RawMouseEventType.LeaveWindow: @@ -133,26 +148,25 @@ namespace Avalonia.Input case RawMouseEventType.LeftButtonDown: case RawMouseEventType.RightButtonDown: case RawMouseEventType.MiddleButtonDown: - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - e.Type == RawMouseEventType.LeftButtonDown - ? MouseButton.Left - : e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle, - e.InputModifiers); + if (ButtonCount(props) > 1) + e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + else + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, + props, e.InputModifiers); break; case RawMouseEventType.LeftButtonUp: case RawMouseEventType.RightButtonUp: case RawMouseEventType.MiddleButtonUp: - e.Handled = MouseUp(mouse, e.Root, e.Position, - e.Type == RawMouseEventType.LeftButtonUp - ? MouseButton.Left - : e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle, - e.InputModifiers); + if (ButtonCount(props) != 0) + e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); + else + e.Handled = MouseUp(mouse, e.Root, e.Position, props, e.InputModifiers); break; case RawMouseEventType.Move: - e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers); + e.Handled = MouseMove(mouse, e.Root, e.Position, props, e.InputModifiers); break; case RawMouseEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); + e.Handled = MouseWheel(mouse, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers); break; } } @@ -165,7 +179,30 @@ namespace Avalonia.Input ClearPointerOver(this, root, inputModifiers); } - private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers) + + PointerPointProperties CreateProperties(RawMouseEventArgs args) + { + var rv = new PointerPointProperties(args.InputModifiers); + + if (args.Type == RawMouseEventType.LeftButtonDown) + rv.IsLeftButtonPressed = true; + if (args.Type == RawMouseEventType.MiddleButtonDown) + rv.IsMiddleButtonPressed = true; + if (args.Type == RawMouseEventType.RightButtonDown) + rv.IsRightButtonPressed = true; + if (args.Type == RawMouseEventType.LeftButtonUp) + rv.IsLeftButtonPressed = false; + if (args.Type == RawMouseEventType.MiddleButtonUp) + rv.IsMiddleButtonPressed = false; + if (args.Type == RawMouseEventType.RightButtonDown) + rv.IsRightButtonPressed = false; + return rv; + } + + private MouseButton _lastMouseDownButton; + private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, + PointerPointProperties properties, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -190,16 +227,8 @@ namespace Avalonia.Input _lastClickTime = timestamp; _lastClickRect = new Rect(p, new Size()) .Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2)); - - var e = new PointerPressedEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerPressedEvent, - Source = source, - ClickCount = _clickCount, - MouseButton = button, - InputModifiers = inputModifiers - }; + _lastMouseDownButton = properties.GetObsoleteMouseButton(); + var e = new PointerPressedEventArgs(source, this, root, p, properties, inputModifiers, _clickCount); source.RaiseEvent(e); return e.Handled; @@ -209,7 +238,8 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers) + private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties properties, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -226,19 +256,15 @@ namespace Avalonia.Input source = Captured; } - var e = new PointerEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerMovedEvent, - Source = source, - InputModifiers = inputModifiers - }; + var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, this, root, + p, properties, inputModifiers); source?.RaiseEvent(e); return e.Handled; } - private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers) + private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, PointerPointProperties props, + InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -248,14 +274,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerReleasedEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerReleasedEvent, - Source = source, - MouseButton = button, - InputModifiers = inputModifiers - }; + var e = new PointerReleasedEventArgs(source, this, root, p, props, inputModifiers, _lastMouseDownButton); source?.RaiseEvent(e); return e.Handled; @@ -264,7 +283,9 @@ namespace Avalonia.Input return false; } - private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers) + private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, + PointerPointProperties props, + Vector delta, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); @@ -274,14 +295,7 @@ namespace Avalonia.Input if (hit != null) { var source = GetSource(hit); - var e = new PointerWheelEventArgs - { - Device = this, - RoutedEvent = InputElement.PointerWheelChangedEvent, - Source = source, - Delta = delta, - InputModifiers = inputModifiers - }; + var e = new PointerWheelEventArgs(source, this, root, p, props, inputModifiers, delta); source?.RaiseEvent(e); return e.Handled; @@ -306,18 +320,19 @@ namespace Avalonia.Input return Captured ?? root.InputHitTest(p); } + PointerEventArgs CreateSimpleEvent(RoutedEvent ev, IInteractive source, InputModifiers inputModifiers) + { + return new PointerEventArgs(ev, source, this, null, default, + new PointerPointProperties(inputModifiers), inputModifiers); + } + private void ClearPointerOver(IPointerDevice device, IInputRoot root, InputModifiers inputModifiers) { Contract.Requires(device != null); Contract.Requires(root != null); var element = root.PointerOverElement; - var e = new PointerEventArgs - { - RoutedEvent = InputElement.PointerLeaveEvent, - Device = device, - InputModifiers = inputModifiers - }; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, element, inputModifiers); if (element!=null && !element.IsAttachedToVisualTree) { @@ -384,7 +399,6 @@ namespace Avalonia.Input IInputElement branch = null; - var e = new PointerEventArgs { Device = device, InputModifiers = inputModifiers }; var el = element; while (el != null) @@ -398,8 +412,8 @@ namespace Avalonia.Input } el = root.PointerOverElement; - e.RoutedEvent = InputElement.PointerLeaveEvent; + var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, el, inputModifiers); if (el!=null && branch!=null && !el.IsAttachedToVisualTree) { ClearChildrenPointerOver(e,branch,false); diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 7b2497c460..91bfb694a0 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.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 Avalonia.Input.Raw; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -8,25 +10,68 @@ namespace Avalonia.Input { public class PointerEventArgs : RoutedEventArgs { - public PointerEventArgs() - { + private readonly IVisual _rootVisual; + private readonly Point _rootVisualPosition; + private readonly PointerPointProperties _properties; + public PointerEventArgs(RoutedEvent routedEvent, + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, + InputModifiers modifiers) + : base(routedEvent) + { + Source = source; + _rootVisual = rootVisual; + _rootVisualPosition = rootVisualPosition; + _properties = properties; + Pointer = pointer; + InputModifiers = modifiers; +#pragma warning disable 612 +#pragma warning disable 618 + Device = new EmulatedDevice(this); +#pragma warning restore 618 +#pragma warning restore 612 } - public PointerEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + class EmulatedDevice : IPointerDevice { + private readonly PointerEventArgs _ev; + public EmulatedDevice(PointerEventArgs ev) + { + _ev = ev; + } + + public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); + + public IInputElement Captured => _ev.Pointer.Captured; + public void Capture(IInputElement control) + { + _ev.Pointer.Capture(control); + } + + public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); } - public IPointerDevice Device { get; set; } + public IPointer Pointer { get; } + + [Obsolete("Use Pointer to get pointer-specific information")] + public IPointerDevice Device { get; } - public InputModifiers InputModifiers { get; set; } + public InputModifiers InputModifiers { get; } public Point GetPosition(IVisual relativeTo) { - return Device.GetPosition(relativeTo); + if (_rootVisual == null) + return default; + if (relativeTo == null) + return _rootVisualPosition; + return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default; } + + public PointerPoint GetPointerPoint(IVisual relativeTo) + => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); } public enum MouseButton @@ -39,32 +84,39 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - public PointerPressedEventArgs() - : base(InputElement.PointerPressedEvent) - { - } + private readonly int _obsoleteClickCount; - public PointerPressedEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + public PointerPressedEventArgs( + IInteractive source, + IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, + InputModifiers modifiers, + int obsoleteClickCount = 1) + : base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition, properties, + modifiers) { + _obsoleteClickCount = obsoleteClickCount; } - public int ClickCount { get; set; } - public MouseButton MouseButton { get; set; } + [Obsolete("Use DoubleTapped or DoubleRightTapped event instead")] + public int ClickCount => _obsoleteClickCount; + + [Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton(); } public class PointerReleasedEventArgs : PointerEventArgs { - public PointerReleasedEventArgs() - : base(InputElement.PointerReleasedEvent) - { - } - - public PointerReleasedEventArgs(RoutedEvent routedEvent) - : base(routedEvent) + public PointerReleasedEventArgs( + IInteractive source, IPointer pointer, + IVisual rootVisual, Point rootVisualPosition, PointerPointProperties properties, InputModifiers modifiers, + MouseButton obsoleteMouseButton) + : base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition, + properties, modifiers) { + MouseButton = obsoleteMouseButton; } - public MouseButton MouseButton { get; set; } + [Obsolete()] + public MouseButton MouseButton { get; private set; } } } diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs new file mode 100644 index 0000000000..7117b5709c --- /dev/null +++ b/src/Avalonia.Input/PointerPoint.cs @@ -0,0 +1,45 @@ +namespace Avalonia.Input +{ + public sealed class PointerPoint + { + public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) + { + Pointer = pointer; + Position = position; + Properties = properties; + } + public IPointer Pointer { get; } + public PointerPointProperties Properties { get; } + public Point Position { get; } + } + + public sealed class PointerPointProperties + { + public bool IsLeftButtonPressed { get; set; } + public bool IsMiddleButtonPressed { get; set; } + public bool IsRightButtonPressed { get; set; } + + public PointerPointProperties() + { + + } + + public PointerPointProperties(InputModifiers modifiers) + { + IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton); + IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton); + IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton); + } + + public MouseButton GetObsoleteMouseButton() + { + if (IsLeftButtonPressed) + return MouseButton.Left; + if (IsMiddleButtonPressed) + return MouseButton.Middle; + if (IsRightButtonPressed) + return MouseButton.Right; + return MouseButton.None; + } + } +} diff --git a/src/Avalonia.Input/PointerWheelEventArgs.cs b/src/Avalonia.Input/PointerWheelEventArgs.cs index 401d1d847c..b409cc81bd 100644 --- a/src/Avalonia.Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Input/PointerWheelEventArgs.cs @@ -1,10 +1,21 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Interactivity; +using Avalonia.VisualTree; + namespace Avalonia.Input { public class PointerWheelEventArgs : PointerEventArgs { public Vector Delta { get; set; } + + public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual, + Point rootVisualPosition, + PointerPointProperties properties, InputModifiers modifiers, Vector delta) + : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, properties, modifiers) + { + Delta = delta; + } } } diff --git a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs b/src/Avalonia.Input/Raw/RawMouseEventArgs.cs index c5637d66cc..42dca6f3a6 100644 --- a/src/Avalonia.Input/Raw/RawMouseEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawMouseEventArgs.cs @@ -17,6 +17,9 @@ namespace Avalonia.Input.Raw Move, Wheel, NonClientLeftButtonDown, + TouchBegin, + TouchUpdate, + TouchEnd } /// diff --git a/src/Avalonia.Input/Raw/RawTouchEventArgs.cs b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs new file mode 100644 index 0000000000..9a266d090d --- /dev/null +++ b/src/Avalonia.Input/Raw/RawTouchEventArgs.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Input.Raw +{ + public class RawTouchEventArgs : RawMouseEventArgs + { + public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root, + RawMouseEventType type, Point position, InputModifiers inputModifiers, + long touchPointId) + : base(device, timestamp, root, type, position, inputModifiers) + { + TouchPointId = touchPointId; + } + + public long TouchPointId { get; set; } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs index 9a751d4953..20b2ac5d9d 100644 --- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs @@ -14,6 +14,8 @@ namespace Avalonia.Controls.UnitTests { public class ButtonTests { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Button_Is_Disabled_When_Command_Is_Disabled() { @@ -102,12 +104,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Raises_Click() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(50, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + var pt = new Point(50, 50); Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -122,15 +120,15 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerEnter(target); + RaisePointerMove(target, pt); + RaisePointerPressed(target, 1, MouseButton.Left, pt); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, pt); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.True(clicked); } @@ -138,12 +136,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(200, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]); @@ -158,16 +152,16 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); - RaisePointerLeave(target, mouse); + RaisePointerEnter(target); + RaisePointerMove(target, new Point(50,50)); + RaisePointerPressed(target, 1, MouseButton.Left, new Point(50, 50)); + RaisePointerLeave(target); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, new Point(200, 50)); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.False(clicked); } @@ -175,12 +169,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Button_With_RenderTransform_Raises_Click() { - var mouse = Mock.Of(); var renderer = Mock.Of(); - IInputElement captured = null; - Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny())).Returns(new Point(150, 50)); - Mock.Get(mouse).Setup(m => m.Capture(It.IsAny())).Callback(v => captured = v); - Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured); + var pt = new Point(150, 50); Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) .Returns>((p, r, f) => r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ? @@ -204,15 +194,15 @@ namespace Avalonia.Controls.UnitTests target.Click += (s, e) => clicked = true; - RaisePointerEnter(target, mouse); - RaisePointerMove(target, mouse); - RaisePointerPressed(target, mouse, 1, MouseButton.Left); + RaisePointerEnter(target); + RaisePointerMove(target, pt); + RaisePointerPressed(target, 1, MouseButton.Left, pt); - Assert.Equal(captured, target); + Assert.Equal(_helper.Captured, target); - RaisePointerReleased(target, mouse, MouseButton.Left); + RaisePointerReleased(target, MouseButton.Left, pt); - Assert.Equal(captured, null); + Assert.Equal(_helper.Captured, null); Assert.True(clicked); } @@ -278,57 +268,29 @@ namespace Avalonia.Controls.UnitTests public PixelPoint PointToScreen(Point p) => throw new NotImplementedException(); } - private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton) + private void RaisePointerPressed(Button button, int clickCount, MouseButton mouseButton, Point position) { - button.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - Source = button, - MouseButton = mouseButton, - ClickCount = clickCount, - Device = device, - }); + _helper.Down(button, mouseButton, position, clickCount: clickCount); } - private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton) + private void RaisePointerReleased(Button button, MouseButton mouseButton, Point pt) { - button.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - Source = button, - MouseButton = mouseButton, - Device = device, - }); + _helper.Up(button, mouseButton, pt); } - private void RaisePointerEnter(Button button, IMouseDevice device) + private void RaisePointerEnter(Button button) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerEnterEvent, - Source = button, - Device = device, - }); + _helper.Enter(button); } - private void RaisePointerLeave(Button button, IMouseDevice device) + private void RaisePointerLeave(Button button) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerLeaveEvent, - Source = button, - Device = device, - }); + _helper.Leave(button); } - private void RaisePointerMove(Button button, IMouseDevice device) + private void RaisePointerMove(Button button, Point pos) { - button.RaiseEvent(new PointerEventArgs - { - RoutedEvent = InputElement.PointerMovedEvent, - Source = button, - Device = device, - }); + _helper.Move(button, pos); } private class TestCommand : ICommand diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index fdbca70350..70ec6c1408 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests { public class ComboBoxTests { + MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void Clicking_On_Control_Toggles_IsDropDownOpen() { @@ -23,17 +25,11 @@ namespace Avalonia.Controls.UnitTests Items = new[] { "Foo", "Bar" }, }; - target.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - }); - + _helper.Down(target); + _helper.Up(target); Assert.True(target.IsDropDownOpen); - target.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - }); + _helper.Down(target); Assert.False(target.IsDropDownOpen); } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 834c49ba6b..067c66969f 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -14,6 +14,7 @@ namespace Avalonia.Controls.UnitTests public class ContextMenuTests { private Mock popupImpl; + private MouseTestHelper _mouse = new MouseTestHelper(); [Fact] public void Clicking_On_Control_Toggles_ContextMenu() @@ -31,19 +32,11 @@ namespace Avalonia.Controls.UnitTests new Window { Content = target }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.None - }); + _mouse.Click(target); Assert.False(sut.IsOpen); popupImpl.Verify(x => x.Show(), Times.Once); @@ -69,19 +62,11 @@ namespace Avalonia.Controls.UnitTests Avalonia.Application.Current.MainWindow = window; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); popupImpl.Verify(x => x.Hide(), Times.Once); @@ -106,11 +91,7 @@ namespace Avalonia.Controls.UnitTests sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(eventCalled); Assert.False(sut.IsOpen); @@ -136,19 +117,11 @@ namespace Avalonia.Controls.UnitTests sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.Right - }); + _mouse.Click(target, MouseButton.Right); Assert.True(sut.IsOpen); - target.RaiseEvent(new PointerReleasedEventArgs - { - RoutedEvent = InputElement.PointerReleasedEvent, - MouseButton = MouseButton.None - }); + _mouse.Click(target, MouseButton.Right); Assert.True(eventCalled); Assert.True(sut.IsOpen); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 343d8d41f3..238e214a5d 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.UnitTests { public class ListBoxTests { + private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Should_Use_ItemTemplate_To_Create_Item_Content() { @@ -225,12 +227,7 @@ namespace Avalonia.Controls.UnitTests private void RaisePressedEvent(ListBox listBox, ListBoxItem item, MouseButton mouseButton) { - listBox.RaiseEvent(new PointerPressedEventArgs - { - Source = item, - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = mouseButton - }); + _mouse.Click(listBox, item, mouseButton); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index 70d59e82c8..de34558ad1 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -18,6 +18,8 @@ namespace Avalonia.Controls.UnitTests { public class ListBoxTests_Single { + MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Focusing_Item_With_Tab_Should_Not_Select_It() { @@ -68,12 +70,7 @@ namespace Avalonia.Controls.UnitTests }; ApplyTemplate(target); - - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -90,11 +87,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -111,11 +104,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -133,11 +122,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(-1, target.SelectedIndex); } @@ -155,11 +140,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 0; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -177,11 +158,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); target.SelectedIndex = 1; - target.Presenter.Panel.Children[0].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(target.Presenter.Panel.Children[0]); Assert.Equal(0, target.SelectedIndex); } @@ -306,4 +283,4 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs new file mode 100644 index 0000000000..8b61f82c28 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/MouseTestHelper.cs @@ -0,0 +1,114 @@ +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.UnitTests +{ + public class MouseTestHelper + { + + class TestPointer : IPointer + { + 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 InputModifiers _pressedButtons; + public IInputElement Captured => _pointer.Captured; + + InputModifiers Convert(MouseButton mouseButton) + => (mouseButton == MouseButton.Left ? InputModifiers.LeftMouseButton + : mouseButton == MouseButton.Middle ? InputModifiers.MiddleMouseButton + : mouseButton == MouseButton.Right ? InputModifiers.RightMouseButton : InputModifiers.None); + + int ButtonCount(PointerPointProperties props) + { + var rv = 0; + if (props.IsLeftButtonPressed) + rv++; + if (props.IsMiddleButtonPressed) + rv++; + if (props.IsRightButtonPressed) + rv++; + return rv; + } + + private MouseButton _pressedButton; + + InputModifiers GetModifiers(InputModifiers modifiers) => modifiers | _pressedButtons; + + 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); + + public void Down(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, + Point position = default, InputModifiers modifiers = default, int clickCount = 1) + { + _pressedButtons |= Convert(mouseButton); + var props = new PointerPointProperties(_pressedButtons); + if (ButtonCount(props) > 1) + Move(target, source, position); + else + { + _pressedButton = mouseButton; + target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, props, + GetModifiers(modifiers), clickCount)); + } + } + + public void Move(IInteractive target, in Point position, InputModifiers modifiers = default) => Move(target, target, position, modifiers); + 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))); + } + + public void Up(IInteractive target, MouseButton mouseButton = MouseButton.Left, Point position = default, + InputModifiers modifiers = default) + => Up(target, target, mouseButton, position, modifiers); + + public void Up(IInteractive target, IInteractive source, MouseButton mouseButton = MouseButton.Left, + Point position = default, InputModifiers modifiers = default) + { + var conv = Convert(mouseButton); + _pressedButtons = (_pressedButtons | conv) ^ conv; + var props = new PointerPointProperties(_pressedButtons); + if (ButtonCount(props) == 0) + target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, props, + GetModifiers(modifiers), _pressedButton)); + else + Move(target, source, position); + } + + public void Click(IInteractive target, MouseButton button = MouseButton.Left, Point position = default, + InputModifiers modifiers = default) + => Click(target, target, button, position, modifiers); + public void Click(IInteractive target, IInteractive source, MouseButton button = MouseButton.Left, + Point position = default, InputModifiers modifiers = default) + { + Down(target, source, button, position, modifiers); + Up(target, source, button, position, modifiers); + } + + public void Enter(IInteractive target) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnterEvent, target, _pointer, (IVisual)target, default, + new PointerPointProperties(_pressedButtons), _pressedButtons)); + } + + public void Leave(IInteractive target) + { + target.RaiseEvent(new PointerEventArgs(InputElement.PointerLeaveEvent, target, _pointer, (IVisual)target, default, + new PointerPointProperties(_pressedButtons), _pressedButtons)); + } + + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index df1846c617..94a95b6db4 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; using Moq; using Xunit; @@ -9,6 +10,28 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { + class FakePointer : IPointer + { + public void Capture(IInputElement control) + { + Captured = control; + } + + public IInputElement Captured { get; set; } + public PointerType Type { get; } + public bool IsPrimary { get; } = true; + } + + static PointerEventArgs CreateArgs(RoutedEvent ev, IInteractive source) + => new PointerEventArgs(ev, source, new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default); + + static PointerPressedEventArgs CreatePressed(IInteractive source) => new PointerPressedEventArgs(source, + new FakePointer(), (IVisual)source, default, new PointerPointProperties {IsLeftButtonPressed = true}, + default); + + static PointerReleasedEventArgs CreateReleased(IInteractive source) => new PointerReleasedEventArgs(source, + new FakePointer(), (IVisual)source, default, new PointerPointProperties(), default, MouseButton.Left); + public class TopLevel { [Fact] @@ -121,7 +144,8 @@ namespace Avalonia.Controls.UnitTests.Platform x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); - var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + + var e = CreatePressed(item); target.PointerPressed(item, e); Mock.Get(menu).Verify(x => x.Close()); @@ -141,7 +165,7 @@ namespace Avalonia.Controls.UnitTests.Platform x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = nextItem }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, nextItem); menu.SetupGet(x => x.SelectedItem).Returns(item); @@ -161,7 +185,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); menu.SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); @@ -176,7 +200,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item); @@ -330,7 +354,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); target.PointerEnter(item, e); @@ -346,7 +370,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); target.PointerEnter(item, e); Mock.Get(item).Verify(x => x.Open(), Times.Never); @@ -366,7 +390,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); var sibling = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsSubMenuOpen == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerEnterItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SubItems).Returns(new[] { item, sibling }); @@ -386,7 +410,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(item); target.PointerLeave(item, e); @@ -403,7 +427,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); var sibling = Mock.Of(x => x.Parent == parentItem); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); Mock.Get(parentItem).SetupGet(x => x.SelectedItem).Returns(sibling); target.PointerLeave(item, e); @@ -419,7 +443,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); - var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var e = CreateArgs(MenuItem.PointerLeaveItemEvent, item); target.PointerLeave(item, e); @@ -434,7 +458,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); - var e = new PointerReleasedEventArgs { MouseButton = MouseButton.Left, Source = item }; + var e = CreateReleased(item); target.PointerReleased(item, e); @@ -452,8 +476,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var childItem = Mock.Of(x => x.Parent == item); - var enter = new PointerEventArgs { RoutedEvent = MenuItem.PointerEnterItemEvent, Source = item }; - var leave = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; + var enter = CreateArgs(MenuItem.PointerEnterItemEvent, item); + var leave = CreateArgs(MenuItem.PointerLeaveItemEvent, item); // Pointer enters item; item is selected. target.PointerEnter(item, enter); @@ -488,7 +512,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); - var e = new PointerPressedEventArgs { MouseButton = MouseButton.Left, Source = item }; + var e = CreatePressed(item); target.PointerPressed(item, e); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 037c16e231..3b5aa53d56 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -22,6 +22,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class SelectingItemsControlTests { + private MouseTestHelper _helper = new MouseTestHelper(); + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -675,12 +677,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - - target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _helper.Down((Interactive)target.Presenter.Panel.Children[1]); var panel = target.Presenter.Panel; @@ -703,11 +700,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.Presenter.Panel.Children[1].RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _helper.Down(target.Presenter.Panel.Children[1]); items.RemoveAt(1); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 15081b184c..b66d6ed11c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -19,6 +19,8 @@ namespace Avalonia.Controls.UnitTests { public class TreeViewTests { + MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Items_Should_Be_Created() { @@ -130,11 +132,7 @@ namespace Avalonia.Controls.UnitTests Assert.NotNull(container); - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - }); + _mouse.Click(container); Assert.Equal(item, target.SelectedItem); Assert.True(container.IsSelected); @@ -165,12 +163,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(container.IsSelected); - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = InputModifiers.Control - }); + _mouse.Click(container, modifiers: InputModifiers.Control); Assert.Null(target.SelectedItem); Assert.False(container.IsSelected); @@ -205,13 +198,8 @@ namespace Avalonia.Controls.UnitTests Assert.True(container1.IsSelected); - container2.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = InputModifiers.Control - }); - + _mouse.Click(container2, modifiers: InputModifiers.Control); + Assert.Equal(item2, target.SelectedItem); Assert.False(container1.IsSelected); Assert.True(container2.IsSelected); @@ -242,15 +230,15 @@ namespace Avalonia.Controls.UnitTests var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1); var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2); - TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control); + ClickContainer(item1Container, InputModifiers.Control); Assert.True(item1Container.IsSelected); - TreeTestHelper.ClickContainer(item2Container, InputModifiers.Control); + ClickContainer(item2Container, InputModifiers.Control); Assert.True(item2Container.IsSelected); Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType()); - TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control); + ClickContainer(item1Container, InputModifiers.Control); Assert.False(item1Container.IsSelected); Assert.DoesNotContain(item1, target.SelectedItems.OfType()); @@ -281,12 +269,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] @@ -314,12 +302,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); } [Fact] @@ -347,12 +335,12 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + ClickContainer(toContainer, InputModifiers.Shift); + AssertChildrenSelected(target, rootNode); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(fromContainer, InputModifiers.None); Assert.True(fromContainer.IsSelected); @@ -656,7 +644,7 @@ namespace Avalonia.Controls.UnitTests target.RaiseEvent(keyEvent); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + AssertChildrenSelected(target, rootNode); } } @@ -687,8 +675,8 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); var keymap = AvaloniaLocator.Current.GetService(); var selectAllGesture = keymap.SelectAll.First(); @@ -702,7 +690,7 @@ namespace Avalonia.Controls.UnitTests target.RaiseEvent(keyEvent); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + AssertChildrenSelected(target, rootNode); } } @@ -733,8 +721,8 @@ namespace Avalonia.Controls.UnitTests var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); - TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None); - TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift); + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); var keymap = AvaloniaLocator.Current.GetService(); var selectAllGesture = keymap.SelectAll.First(); @@ -748,7 +736,7 @@ namespace Avalonia.Controls.UnitTests target.RaiseEvent(keyEvent); - TreeTestHelper.AssertChildrenSelected(target, rootNode); + AssertChildrenSelected(target, rootNode); } } @@ -871,29 +859,22 @@ namespace Avalonia.Controls.UnitTests } } - private static class TreeTestHelper + void ClickContainer(IControl container, InputModifiers modifiers) { - public static void ClickContainer(IControl container, InputModifiers modifiers) - { - container.RaiseEvent(new PointerPressedEventArgs - { - RoutedEvent = InputElement.PointerPressedEvent, - MouseButton = MouseButton.Left, - InputModifiers = modifiers - }); - } + _mouse.Click(container, modifiers: modifiers); + } - public static void AssertChildrenSelected(TreeView treeView, Node rootNode) + void AssertChildrenSelected(TreeView treeView, Node rootNode) + { + foreach (var child in rootNode.Children) { - foreach (var child in rootNode.Children) - { - var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); + var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child); - Assert.True(container.IsSelected); - } + Assert.True(container.IsSelected); } } + private class Node : NotifyingBase { private IAvaloniaList _children; diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 6b19d81034..7316b1de3d 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -4,6 +4,7 @@ netcoreapp2.0;net47 Library true + latest @@ -19,6 +20,7 @@ + diff --git a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs index 931c1b5268..d40e08e748 100644 --- a/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs +++ b/tests/Avalonia.Interactivity.UnitTests/GestureTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Controls; +using Avalonia.Controls.UnitTests; using Avalonia.Input; using Xunit; @@ -10,6 +11,8 @@ namespace Avalonia.Interactivity.UnitTests { public class GestureTests { + private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] public void Tapped_Should_Follow_Pointer_Pressed_Released() { @@ -27,8 +30,7 @@ namespace Avalonia.Interactivity.UnitTests border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.RaiseEvent(new PointerPressedEventArgs()); - border.RaiseEvent(new PointerReleasedEventArgs()); + _mouse.Click(border); Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); } @@ -47,8 +49,7 @@ namespace Avalonia.Interactivity.UnitTests decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); - border.RaiseEvent(new PointerPressedEventArgs()); - border.RaiseEvent(new PointerReleasedEventArgs()); + _mouse.Click(border); Assert.Equal(new[] { "bt", "dt" }, result); } @@ -72,9 +73,8 @@ namespace Avalonia.Interactivity.UnitTests border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); - border.RaiseEvent(new PointerPressedEventArgs()); - border.RaiseEvent(new PointerReleasedEventArgs()); - border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + _mouse.Click(border); + _mouse.Down(border, clickCount: 2); Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 795aba153a..3511919e39 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -137,8 +137,9 @@ namespace Avalonia.Markup.Xaml.UnitTests .GetVisualChildren().First() .GetVisualChildren().First() .GetVisualChildren().First(); - ((Control)item).RaiseEvent(new PointerPressedEventArgs {ClickCount = 20}); - Assert.Equal(20, w.Args.ClickCount); + + ((Control)item).DataContext = "test"; + Assert.Equal("test", w.SavedContext); } } @@ -161,10 +162,10 @@ namespace Avalonia.Markup.Xaml.UnitTests public class XamlIlBugTestsEventHandlerCodeBehind : Window { - public PointerPressedEventArgs Args; - public void HandlePointerPressed(object sender, PointerPressedEventArgs args) + public object SavedContext; + public void HandleDataContextChanged(object sender, EventArgs args) { - Args = args; + SavedContext = ((Control)sender).DataContext; } public XamlIlBugTestsEventHandlerCodeBehind() @@ -178,7 +179,7 @@ namespace Avalonia.Markup.Xaml.UnitTests -