| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- 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
- {
- internal sealed class SharedSizeScopeHost : IDisposable
- {
- private enum MeasurementState
- {
- Invalidated,
- Measuring,
- Cached
- }
- 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<DefinitionBase>()
- .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));
- }
- private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> 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<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>();
- var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0);
- 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<DefinitionBase>()
- .Concat(Grid.ColumnDefinitions)
- .Select(d => new MeasurementResult(Grid, d))
- .ToList();
- NotifyOldItems();
- NotifyNewItems();
- break;
- }
- }
- 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];
- }
- for (int i = 0; i < Grid.ColumnDefinitions.Count; i++)
- {
- Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i];
- }
- }
- public void InvalidateMeasure()
- {
- MeasurementState = MeasurementState.Invalidated;
- Results.ForEach(r =>
- {
- r.MeasuredResult = double.NaN;
- r.SizeGroup?.Reset();
- });
- }
- public void Dispose()
- {
- _subscriptions.Dispose();
- _groupChanged.OnCompleted();
- }
- public Grid Grid { get; }
- public MeasurementState MeasurementState { get; private set; }
- public List<MeasurementResult> Results { get; private set; }
- }
- private class MeasurementResult
- {
- public MeasurementResult(Grid owningGrid, DefinitionBase definition)
- {
- OwningGrid = owningGrid;
- Definition = definition;
- MeasuredResult = double.NaN;
- }
- public DefinitionBase Definition { get; }
- public double MeasuredResult { get; set; }
- public Group SizeGroup { get; set; }
- public Grid OwningGrid { get; }
- }
- private class Group
- {
- private double? cachedResult;
- private List<MeasurementResult> _results = new List<MeasurementResult>();
- public string Name { get; }
- public Group(string name)
- {
- Name = name;
- }
- public bool IsFixed { get; set; }
- public IReadOnlyList<MeasurementResult> Results => _results;
- public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value;
- public void Reset()
- {
- cachedResult = null;
- }
- public void Add(MeasurementResult result)
- {
- if (_results.Contains(result))
- throw new AvaloniaInternalException(
- $"Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result");
- result.SizeGroup = this;
- _results.Add(result);
- }
- public void Remove(MeasurementResult result)
- {
- if (!_results.Contains(result))
- throw new AvaloniaInternalException(
- $"Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result");
- result.SizeGroup = null;
- _results.Remove(result);
- }
- private double Gather()
- {
- var result = 0.0d;
- bool onlyFixed = false;
- foreach (var measurement in Results)
- {
- if (Double.IsInfinity(measurement.MeasuredResult))
- continue;
- if (measurement.Definition is ColumnDefinition column)
- {
- if (!onlyFixed && column.Width.IsAbsolute)
- {
- onlyFixed = true;
- result = measurement.MeasuredResult;
- }
- else if (onlyFixed == column.Width.IsAbsolute)
- result = Math.Max(result, measurement.MeasuredResult);
- result = Math.Max(result, column.MinWidth);
- }
- if (measurement.Definition is RowDefinition row)
- {
- if (!onlyFixed && row.Height.IsAbsolute)
- {
- onlyFixed = true;
- result = measurement.MeasuredResult;
- }
- else if (onlyFixed == row.Height.IsAbsolute)
- result = Math.Max(result, measurement.MeasuredResult);
- result = Math.Max(result, row.MinHeight);
- }
- }
- return result;
- }
- }
- private readonly AvaloniaList<MeasurementCache> _measurementCaches;
- private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>();
- public SharedSizeScopeHost(Control scope)
- {
- _measurementCaches = GetParticipatingGrids(scope);
- foreach (var cache in _measurementCaches)
- {
- cache.Grid.InvalidateMeasure();
- AddGridToScopes(cache);
- }
- }
- void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change)
- {
- RemoveFromGroup(change.oldName, change.result);
- AddToGroup(change.newName, change.result);
- }
- private bool _invalidating;
- internal void InvalidateMeasure(Grid grid)
- {
- // prevent stack overflow
- if (_invalidating)
- return;
- _invalidating = true;
- InvalidateMeasureImpl(grid);
- _invalidating = false;
- }
- private void InvalidateMeasureImpl(Grid grid)
- {
- var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
- if (cache == null)
- throw new AvaloniaInternalException(
- $"InvalidateMeasureImpl - called with a grid not present in the internal cache");
- // already invalidated the cache, early out.
- if (cache.MeasurementState == MeasurementState.Invalidated)
- 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);
- }
- }
- internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
- {
- var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
- Debug.Assert(cache != null);
- cache.UpdateMeasureResult(rowResult, columnResult);
- }
- (List<GridLayout.LengthConvention>, List<double>, double) Arrange(IReadOnlyList<DefinitionBase> 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];
- if (string.IsNullOrEmpty(definition.SharedSizeGroup))
- {
- desiredLength += measureResult.LengthList[i];
- continue;
- }
- var group = _groups[definition.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 (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),
- new GridLayout.MeasureResult(
- columnResult.ContainerLength,
- columnDesiredLength,
- columnResult.GreedyDesiredLength, //??
- columnConventions,
- columnLengths)
- );
- }
- private void AddGridToScopes(MeasurementCache cache)
- {
- cache.GroupChanged.Subscribe(SharedGroupChanged);
- foreach (var result in cache.Results)
- {
- var scopeName = result.Definition.SharedSizeGroup;
- AddToGroup(scopeName, result);
- }
- }
- 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.IsFixed |= IsFixed(result.Definition);
- group.Add(result);
- }
- private bool IsFixed(DefinitionBase definition)
- {
- return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute;
- }
- private void RemoveGridFromScopes(MeasurementCache cache)
- {
- foreach (var result in cache.Results)
- {
- var scopeName = result.Definition.SharedSizeGroup;
- RemoveFromGroup(scopeName, result);
- }
- }
- private void RemoveFromGroup(string scopeName, MeasurementResult result)
- {
- if (string.IsNullOrEmpty(scopeName))
- return;
- Debug.Assert(_groups.TryGetValue(scopeName, out var group));
- group.Remove(result);
- if (!group.Results.Any())
- _groups.Remove(scopeName);
- else
- {
- group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed);
- }
- }
- private static AvaloniaList<MeasurementCache> GetParticipatingGrids(Control scope)
- {
- var result = scope.GetVisualDescendants().OfType<Grid>();
- return new AvaloniaList<MeasurementCache>(
- 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)
- {
- Debug.Assert(!_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd)));
- var cache = new MeasurementCache(toAdd);
- _measurementCaches.Add(cache);
- AddGridToScopes(cache);
- }
- internal void UnegisterGrid(Grid toRemove)
- {
- var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove));
- Debug.Assert(cache != null);
- _measurementCaches.Remove(cache);
- RemoveGridFromScopes(cache);
- cache.Dispose();
- }
- }
- }
|