From a33f5cb4dd5fb1410d26c31133090275b4edf480 Mon Sep 17 00:00:00 2001 From: Wojciech Krysiak Date: Sat, 27 Oct 2018 12:42:42 +0200 Subject: [PATCH] Some unit tests, bugfixes and refactorings. --- src/Avalonia.Controls/ColumnDefinition.cs | 2 +- src/Avalonia.Controls/Grid.cs | 159 ++++---- src/Avalonia.Controls/GridSplitter.cs | 6 + .../Utils/SharedSizeScopeHost.cs | 340 ++++++++++++------ .../SharedSizeScopeTests.cs | 191 ++++++++++ 5 files changed, 532 insertions(+), 166 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs diff --git a/src/Avalonia.Controls/ColumnDefinition.cs b/src/Avalonia.Controls/ColumnDefinition.cs index a6b34f8a16..d316881a05 100644 --- a/src/Avalonia.Controls/ColumnDefinition.cs +++ b/src/Avalonia.Controls/ColumnDefinition.cs @@ -88,4 +88,4 @@ namespace Avalonia.Controls set { SetValue(WidthProperty, value); } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7bda4140a3..176c8cdb89 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -62,8 +62,8 @@ namespace Avalonia.Controls /// Defines the SharedSizeScopeHost private property. /// The ampersands are used to make accessing the property via xaml inconvenient. /// - private static readonly AttachedProperty s_sharedSizeScopeHostProperty = - AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost", null); + internal static readonly AttachedProperty s_sharedSizeScopeHostProperty = + AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost"); private ColumnDefinitions _columnDefinitions; @@ -81,49 +81,6 @@ namespace Avalonia.Controls this.DetachedFromVisualTree += Grid_DetachedFromVisualTree; } - 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); - } - } - - private void Grid_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e) - { - _sharedSizeHost?.UnegisterGrid(this); - _sharedSizeHost = null; - } - - 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.SetValue(s_sharedSizeScopeHostProperty, null); - } - - 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(source)); - } - } - /// /// Gets or sets the columns definitions for the grid. /// @@ -400,7 +357,7 @@ namespace Avalonia.Controls var columnCache = _columnMeasureCache; if (_sharedSizeHost?.ParticipatesInScope(this) ?? false) - { + { (rowCache, columnCache) = _sharedSizeHost.HandleArrange(this, _rowMeasureCache, _columnMeasureCache); rowCache = rowLayout.Measure(finalSize.Height, rowCache.LeanLengthList); @@ -438,6 +395,73 @@ namespace Avalonia.Controls 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. @@ -515,25 +539,40 @@ namespace Avalonia.Controls return value; } - internal bool HasSharedSizeGroups() - { - return ColumnDefinitions.Any(cd => !string.IsNullOrEmpty(cd.SharedSizeGroup)) || - RowDefinitions.Any(rd => !string.IsNullOrEmpty(rd.SharedSizeGroup)); - } - - internal void SharedScopeChanged() + /// + /// 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) { - _sharedSizeHost = null; - var scope = this.GetVisualAncestors().OfType() - .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + 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); + } - if (scope != null) + var shouldAssign = (arg2.NewValue is bool a) && a; + if (shouldAssign) { - _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); - _sharedSizeHost.RegisterGrid(this); + 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()); } - InvalidateMeasure(); + // 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/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 4d38d7389e..304a760216 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -44,6 +44,12 @@ namespace Avalonia.Controls protected override void OnDragDelta(VectorEventArgs e) { + // WPF doesn't change anything when spliter is in the last row/column + // but resizes the splitter row/column when it's the first one. + // this is different, but more internally consistent. + if (_prevDefinition == null || _nextDefinition == null) + return; + var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y; double max; double min; diff --git a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs index 5948bd7f19..ec9c0b3eca 100644 --- a/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs +++ b/src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs @@ -13,6 +13,12 @@ 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 @@ -22,6 +28,12 @@ namespace Avalonia.Controls 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; @@ -129,6 +141,12 @@ namespace Avalonia.Controls } } + + /// + /// 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; @@ -145,9 +163,16 @@ namespace Avalonia.Controls } } + /// + /// 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; @@ -155,18 +180,38 @@ namespace Avalonia.Controls }); } + /// + /// 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) @@ -176,12 +221,35 @@ namespace Avalonia.Controls 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; @@ -196,12 +264,24 @@ namespace Avalonia.Controls } } - + /// + /// 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(); @@ -221,12 +301,17 @@ namespace Avalonia.Controls } } - + /// + /// 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) @@ -234,17 +319,29 @@ namespace Avalonia.Controls Name = name; } - public bool IsFixed { get; set; } - + /// + /// 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)) @@ -255,6 +352,10 @@ namespace Avalonia.Controls _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)) @@ -275,30 +376,64 @@ namespace Avalonia.Controls } } - private readonly AvaloniaList _measurementCaches; - + private readonly AvaloniaList _measurementCaches = new AvaloniaList(); private readonly Dictionary _groups = new Dictionary(); + private bool _invalidating; - public SharedSizeScopeHost(Control scope) + /// + /// Removes the SharedSizeScope and notifies all affected grids of the change. + /// + public void Dispose() { - _measurementCaches = GetParticipatingGrids(scope); + while (_measurementCaches.Any()) + _measurementCaches[0].Grid.SharedScopeChanged(); + } - foreach (var cache in _measurementCaches) - { - cache.Grid.InvalidateMeasure(); - AddGridToScopes(cache); + /// + /// 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); } - void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change) + /// + /// Removes the registration for a grid in this SharedSizeScope. + /// + /// The to remove. + internal void UnegisterGrid(Grid toRemove) { - RemoveFromGroup(change.oldName, change.result); - AddToGroup(change.newName, change.result); + 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(); } - private bool _invalidating; + /// + /// 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 @@ -311,6 +446,40 @@ namespace Avalonia.Controls _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)); @@ -323,6 +492,10 @@ namespace Avalonia.Controls 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? @@ -346,16 +519,24 @@ namespace Avalonia.Controls } } - internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) + /// + /// 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) { - 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); + RemoveFromGroup(change.oldName, change.result); + AddToGroup(change.newName, change.result); } - (List, List, double) Arrange(IReadOnlyList definitions, GridLayout.MeasureResult measureResult) + /// + /// 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(); @@ -363,6 +544,8 @@ namespace Avalonia.Controls 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]; @@ -370,7 +553,7 @@ namespace Avalonia.Controls } var group = _groups[definition.SharedSizeGroup]; - + // Length calculated over all Definitions participating in a SharedSizeGroup. var length = group.CalculatedLength; conventions[i] = new GridLayout.LengthConvention( @@ -382,33 +565,19 @@ namespace Avalonia.Controls desiredLength += length; } - return (conventions, lengths, desiredLength); - } - - internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult) - { - var (rowConventions, rowLengths, rowDesiredLength) = Arrange(grid.RowDefinitions, rowResult); - var (columnConventions, columnLengths, columnDesiredLength) = Arrange(grid.ColumnDefinitions, columnResult); - - return ( - new GridLayout.MeasureResult( - rowResult.ContainerLength, - rowDesiredLength, - rowResult.GreedyDesiredLength,//?? - rowConventions, - rowLengths, - rowResult.MinLengths), - new GridLayout.MeasureResult( - columnResult.ContainerLength, - columnDesiredLength, - columnResult.GreedyDesiredLength, //?? - columnConventions, - columnLengths, - columnResult.MinLengths) - ); + 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); @@ -420,6 +589,12 @@ namespace Avalonia.Controls } } + /// + /// 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)) @@ -428,16 +603,13 @@ namespace Avalonia.Controls if (!_groups.TryGetValue(scopeName, out var group)) _groups.Add(scopeName, group = new Group(scopeName)); - group.IsFixed |= IsFixed(result.Definition); - group.Add(result); } - private bool IsFixed(DefinitionBase definition) - { - return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute; - } - + /// + /// 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) @@ -447,6 +619,12 @@ namespace Avalonia.Controls } } + /// + /// 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)) @@ -458,54 +636,6 @@ namespace Avalonia.Controls group.Remove(result); if (!group.Results.Any()) _groups.Remove(scopeName); - else - { - group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed); - } - } - - private static AvaloniaList GetParticipatingGrids(Control scope) - { - var result = scope.GetVisualDescendants().OfType(); - - return new AvaloniaList( - result.Where(g => g.HasSharedSizeGroups()) - .Select(g => new MeasurementCache(g))); - } - - public void Dispose() - { - foreach (var cache in _measurementCaches) - { - cache.Grid.SharedScopeChanged(); - cache.Dispose(); - } - } - - 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); - } - - 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(); - } - - internal bool ParticipatesInScope(Grid toCheck) - { - return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck))?.Results.Any() ?? false; } } } diff --git a/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs new file mode 100644 index 0000000000..0d0b9c2891 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/SharedSizeScopeTests.cs @@ -0,0 +1,191 @@ +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Platform; +using Avalonia.UnitTests; + +using Moq; + +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class SharedSizeScopeTests + { + public SharedSizeScopeTests() + { + } + + [Fact] + public void All_Descendant_Grids_Are_Registered_When_Added_After_Setting_Scope() + { + var grids = new[] { new Grid(), new Grid(), new Grid() }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); + } + + [Fact] + public void All_Descendant_Grids_Are_Registered_When_Setting_Scope() + { + var grids = new[] { new Grid(), new Grid(), new Grid() }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.Child = scope; + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + + Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); + } + + [Fact] + public void All_Descendant_Grids_Are_Unregistered_When_Resetting_Scope() + { + var grids = new[] { new Grid(), new Grid(), new Grid() }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + Assert.All(grids, g => Assert.True(g.HasSharedSizeScope())); + root.SetValue(Grid.IsSharedSizeScopeProperty, false); + Assert.All(grids, g => Assert.False(g.HasSharedSizeScope())); + Assert.Equal(null, root.GetValue(Grid.s_sharedSizeScopeHostProperty)); + } + + [Fact] + public void Size_Is_Propagated_Between_Grids() + { + var grids = new[] { CreateGrid("A", null),CreateGrid(("A",new GridLength(30)), (null, new GridLength()))}; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Size_Propagation_Is_Constrained_To_Innermost_Scope() + { + var grids = new[] { CreateGrid("A", null), CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var innerScope = new Panel(); + innerScope.Children.AddRange(grids); + innerScope.SetValue(Grid.IsSharedSizeScopeProperty, true); + + var outerGrid = CreateGrid(("A", new GridLength(0))); + var outerScope = new Panel(); + outerScope.Children.AddRange(new[] { outerGrid, innerScope }); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = outerScope; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(0, outerGrid.ColumnDefinitions[0].ActualWidth); + } + + [Fact] + public void Size_Is_Propagated_Between_Rows_And_Columns() + { + var grid = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,30"), + RowDefinitions = new RowDefinitions("*,10") + }; + + grid.ColumnDefinitions[1].SharedSizeGroup = "A"; + grid.RowDefinitions[1].SharedSizeGroup = "A"; + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = grid; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(30, grid.RowDefinitions[1].ActualHeight); + } + + [Fact] + public void Size_Group_Changes_Are_Tracked() + { + var grids = new[] { + CreateGrid((null, new GridLength(0, GridUnitType.Auto)), (null, new GridLength())), + CreateGrid(("A", new GridLength(30)), (null, new GridLength())) }; + var scope = new Panel(); + scope.Children.AddRange(grids); + + var root = new TestRoot(); + root.SetValue(Grid.IsSharedSizeScopeProperty, true); + root.Child = scope; + + root.Measure(new Size(50, 50)); + root.Arrange(new Rect(new Point(), new Point(50, 50))); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = "A"; + + root.Measure(new Size(51, 51)); + root.Arrange(new Rect(new Point(), new Point(51, 51))); + Assert.Equal(30, grids[0].ColumnDefinitions[0].ActualWidth); + + grids[0].ColumnDefinitions[0].SharedSizeGroup = null; + + root.Measure(new Size(52, 52)); + root.Arrange(new Rect(new Point(), new Point(52, 52))); + Assert.Equal(0, grids[0].ColumnDefinitions[0].ActualWidth); + } + + // 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; + } + } +}