diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 1a07ccaf7e..71ae414a4f 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Collections; using Avalonia.Controls.Utils; +using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -44,6 +46,189 @@ namespace Avalonia.Controls public static readonly AttachedProperty RowSpanProperty = AvaloniaProperty.RegisterAttached("RowSpan", 1); + public static readonly AttachedProperty IsSharedSizeScopeProperty = + AvaloniaProperty.RegisterAttached("IsSharedSizeScope", false); + + private sealed class SharedSizeScopeHost : IDisposable + { + private class GridMeasureCache + { + public Grid Grid { get; } + public DefinitionBase Definition { get; } + public double CachedLength { get; set; } + } + + private readonly AvaloniaList _participatingGrids; + + private Dictionary _cachedSize = new Dictionary(); + + private Dictionary> _gridsInScopes = new Dictionary>(); + + private Dictionary> _scopeCache; + private int _leftToMeasure; + + public SharedSizeScopeHost(Control scope) + { + _participatingGrids = GetParticipatingGrids(scope); + + foreach (var grid in _participatingGrids) + { + grid.InvalidateMeasure(); + AddGridToScopes(grid); + } + } + + private bool _invalidating = false; + + internal void InvalidateMeasure(Grid grid) + { + if (_invalidating) + return; + _invalidating = true; + + List candidates = new List {grid}; + while (candidates.Any()) + { + var scopes = candidates.SelectMany(c => c.RowDefinitions.Select(rd => rd.SharedSizeGroup)) + .Concat(candidates.SelectMany(c => c.ColumnDefinitions.Select(rd => rd.SharedSizeGroup))).Distinct(); + + candidates = scopes.SelectMany(r => _scopeCache[r].Select(gmc => gmc.Grid)) + .Distinct().Where(c => c.IsMeasureValid).ToList(); + candidates.ForEach(c => c.InvalidateMeasure()); + } + + _invalidating = false; + } + + private void AddGridToScopes(Grid grid) + { + var scopeNames = grid.ColumnDefinitions.Select(g => g.SharedSizeGroup) + .Concat(grid.RowDefinitions.Select(g => g.SharedSizeGroup)).Distinct(); + foreach (var scopeName in scopeNames) + { + if (!_gridsInScopes.TryGetValue(scopeName, out var list)) + _gridsInScopes.Add(scopeName, list = new List() ); + list.Add(grid); + } + } + + private void RemoveGridFromScopes(Grid grid) + { + var scopeNames = grid.ColumnDefinitions.Select(g => g.SharedSizeGroup) + .Concat(grid.RowDefinitions.Select(g => g.SharedSizeGroup)).Distinct(); + foreach (var scopeName in scopeNames) + { + Debug.Assert(_gridsInScopes.TryGetValue(scopeName, out var list)); + list.Remove(grid); + if (!list.Any()) + _gridsInScopes.Remove(scopeName); + } + } + + internal void UpdateMeasureResult(GridLayout.MeasureResult result, ColumnDefinitions columnDefinitions) + { + for (var i = 0; i < columnDefinitions.Count; i++) + { + if (string.IsNullOrEmpty(columnDefinitions[i].SharedSizeGroup)) + continue; + // if any in this group is Absolute we don't care about measured values. + + } + } + + internal void UpdateMeasureResult(GridLayout.MeasureResult result, RowDefinitions rowDefinitions) + { + + } + + internal double GetExistingLimit(DefinitionBase definition) + { + List cache = _scopeCache[definition.SharedSizeGroup]; + + return cache.Where(gmc => gmc.Grid.IsMeasureValid) + .Aggregate(double.NaN, (a, gmc) => Math.Max(a, gmc.CachedLength)); + } + + internal void UpdateExistingLimit(DefinitionBase definition, double limit) + { + List cache = _scopeCache[definition.SharedSizeGroup]; + + cache.Single(gmc => ReferenceEquals(gmc.Definition, definition)).CachedLength = limit; + // if any other are lower - invalidate the grid. + } + + internal void BeginMeasurePass() + { + if (_leftToMeasure == 0) + { + _leftToMeasure = _participatingGrids.Count(g => !g.IsMeasureValid); + } + } + + private static AvaloniaList GetParticipatingGrids(Control scope) + { + var result = scope.GetVisualDescendants().OfType(); + + return new AvaloniaList(result.Where(g => g.HasSharedSizeGroups())); + } + + public void Dispose() + { + foreach (var grid in _participatingGrids) + { + grid.SharedScopeChanged(); + } + } + + internal void RegisterGrid(Grid toAdd) + { + Debug.Assert(!_participatingGrids.Contains(toAdd)); + _participatingGrids.Add(toAdd); + AddGridToScopes(toAdd); + } + + internal void UnegisterGrid(Grid toRemove) + { + Debug.Assert(_participatingGrids.Contains(toRemove)); + _participatingGrids.Remove(toRemove); + RemoveGridFromScopes(toRemove); + } + } + + protected override void OnMeasureInvalidated() + { + base.OnMeasureInvalidated(); + _sharedSizeHost?.InvalidateMeasure(this); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var scope = this.GetVisualAncestors().OfType() + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + Debug.Assert(_sharedSizeHost == null); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + _sharedSizeHost?.UnegisterGrid(this); + _sharedSizeHost = null; + } + + private SharedSizeScopeHost _sharedSizeHost; + + private static readonly AttachedProperty s_sharedSizeScopeHostProperty = + AvaloniaProperty.RegisterAttached("&&SharedSizeScopeHost", null); + private ColumnDefinitions _columnDefinitions; private RowDefinitions _rowDefinitions; @@ -51,6 +236,23 @@ namespace Avalonia.Controls static Grid() { AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + IsSharedSizeScopeProperty.Changed.AddClassHandler(IsSharedSizeScopeChanged); + } + + private static void IsSharedSizeScopeChanged(Control source, AvaloniaPropertyChangedEventArgs arg2) + { + if ((bool)arg2.NewValue) + { + Debug.Assert(source.GetValue(s_sharedSizeScopeHostProperty) == null); + source.SetValue(IsSharedSizeScopeProperty, new SharedSizeScopeHost(source)); + } + else + { + var host = source.GetValue(s_sharedSizeScopeHostProperty) as SharedSizeScopeHost; + Debug.Assert(host != null); + host.Dispose(); + source.SetValue(IsSharedSizeScopeProperty, null); + } } /// @@ -426,5 +628,26 @@ 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() + { + _sharedSizeHost = null; + var scope = this.GetVisualAncestors().OfType() + .FirstOrDefault(c => c.GetValue(IsSharedSizeScopeProperty)); + + if (scope != null) + { + _sharedSizeHost = scope.GetValue(s_sharedSizeScopeHostProperty); + _sharedSizeHost.RegisterGrid(this); + } + + InvalidateMeasure(); + } } }