SharedSizeScopeHost.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.ComponentModel;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Reactive.Disposables;
  8. using System.Reactive.Subjects;
  9. using Avalonia.Collections;
  10. using Avalonia.Controls.Utils;
  11. using Avalonia.Layout;
  12. using Avalonia.VisualTree;
  13. namespace Avalonia.Controls
  14. {
  15. internal sealed class SharedSizeScopeHost : IDisposable
  16. {
  17. private enum MeasurementState
  18. {
  19. Invalidated,
  20. Measuring,
  21. Cached
  22. }
  23. private sealed class MeasurementCache : IDisposable
  24. {
  25. readonly CompositeDisposable _subscriptions;
  26. readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>();
  27. public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged;
  28. public MeasurementCache(Grid grid)
  29. {
  30. Grid = grid;
  31. Results = grid.RowDefinitions.Cast<DefinitionBase>()
  32. .Concat(grid.ColumnDefinitions)
  33. .Select(d => new MeasurementResult(grid, d))
  34. .ToList();
  35. grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged;
  36. grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged;
  37. _subscriptions = new CompositeDisposable(
  38. Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
  39. Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
  40. grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged),
  41. grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged));
  42. }
  43. private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> propertyChanged)
  44. {
  45. if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup))
  46. {
  47. var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1));
  48. var oldName = result.SizeGroup?.Name;
  49. var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup;
  50. _groupChanged.OnNext((oldName, newName, result));
  51. }
  52. }
  53. private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  54. {
  55. int offset = 0;
  56. if (sender is ColumnDefinitions)
  57. offset = Grid.RowDefinitions.Count;
  58. var newItems = e.NewItems?.OfType<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>();
  59. var oldItems = Results.GetRange(e.OldStartingIndex + offset, e.OldItems?.Count ?? 0);
  60. void NotifyNewItems()
  61. {
  62. foreach (var item in newItems)
  63. {
  64. if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
  65. continue;
  66. _groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item));
  67. }
  68. }
  69. void NotifyOldItems()
  70. {
  71. foreach (var item in oldItems)
  72. {
  73. if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
  74. continue;
  75. _groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item));
  76. }
  77. }
  78. switch (e.Action)
  79. {
  80. case NotifyCollectionChangedAction.Add:
  81. Results.InsertRange(e.NewStartingIndex + offset, newItems);
  82. NotifyNewItems();
  83. break;
  84. case NotifyCollectionChangedAction.Remove:
  85. Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
  86. NotifyOldItems();
  87. break;
  88. case NotifyCollectionChangedAction.Move:
  89. Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
  90. Results.InsertRange(e.NewStartingIndex + offset, oldItems);
  91. break;
  92. case NotifyCollectionChangedAction.Replace:
  93. Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
  94. Results.InsertRange(e.NewStartingIndex + offset, newItems);
  95. NotifyOldItems();
  96. NotifyNewItems();
  97. break;
  98. case NotifyCollectionChangedAction.Reset:
  99. oldItems = Results;
  100. newItems = Results = Grid.RowDefinitions.Cast<DefinitionBase>()
  101. .Concat(Grid.ColumnDefinitions)
  102. .Select(d => new MeasurementResult(Grid, d))
  103. .ToList();
  104. NotifyOldItems();
  105. NotifyNewItems();
  106. break;
  107. }
  108. }
  109. public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
  110. {
  111. MeasurementState = MeasurementState.Cached;
  112. for (int i = 0; i < Grid.RowDefinitions.Count; i++)
  113. {
  114. Results[i].MeasuredResult = rowResult.LengthList[i];
  115. }
  116. for (int i = 0; i < Grid.ColumnDefinitions.Count; i++)
  117. {
  118. Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i];
  119. }
  120. }
  121. public void InvalidateMeasure()
  122. {
  123. MeasurementState = MeasurementState.Invalidated;
  124. Results.ForEach(r =>
  125. {
  126. r.MeasuredResult = double.NaN;
  127. r.SizeGroup?.Reset();
  128. });
  129. }
  130. public void Dispose()
  131. {
  132. _subscriptions.Dispose();
  133. _groupChanged.OnCompleted();
  134. }
  135. public Grid Grid { get; }
  136. public MeasurementState MeasurementState { get; private set; }
  137. public List<MeasurementResult> Results { get; private set; }
  138. }
  139. private class MeasurementResult
  140. {
  141. public MeasurementResult(Grid owningGrid, DefinitionBase definition)
  142. {
  143. OwningGrid = owningGrid;
  144. Definition = definition;
  145. MeasuredResult = double.NaN;
  146. }
  147. public DefinitionBase Definition { get; }
  148. public double MeasuredResult { get; set; }
  149. public Group SizeGroup { get; set; }
  150. public Grid OwningGrid { get; }
  151. }
  152. private class Group
  153. {
  154. private double? cachedResult;
  155. private List<MeasurementResult> _results = new List<MeasurementResult>();
  156. public string Name { get; }
  157. public Group(string name)
  158. {
  159. Name = name;
  160. }
  161. public bool IsFixed { get; set; }
  162. public IReadOnlyList<MeasurementResult> Results => _results;
  163. public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value;
  164. public void Reset()
  165. {
  166. cachedResult = null;
  167. }
  168. public void Add(MeasurementResult result)
  169. {
  170. if (_results.Contains(result))
  171. throw new AvaloniaInternalException(
  172. $"Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result");
  173. result.SizeGroup = this;
  174. _results.Add(result);
  175. }
  176. public void Remove(MeasurementResult result)
  177. {
  178. if (!_results.Contains(result))
  179. throw new AvaloniaInternalException(
  180. $"Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result");
  181. result.SizeGroup = null;
  182. _results.Remove(result);
  183. }
  184. private double Gather()
  185. {
  186. var result = 0.0d;
  187. bool onlyFixed = false;
  188. foreach (var measurement in Results)
  189. {
  190. if (Double.IsInfinity(measurement.MeasuredResult))
  191. continue;
  192. if (measurement.Definition is ColumnDefinition column)
  193. {
  194. if (!onlyFixed && column.Width.IsAbsolute)
  195. {
  196. onlyFixed = true;
  197. result = measurement.MeasuredResult;
  198. }
  199. else if (onlyFixed == column.Width.IsAbsolute)
  200. result = Math.Max(result, measurement.MeasuredResult);
  201. result = Math.Max(result, column.MinWidth);
  202. }
  203. if (measurement.Definition is RowDefinition row)
  204. {
  205. if (!onlyFixed && row.Height.IsAbsolute)
  206. {
  207. onlyFixed = true;
  208. result = measurement.MeasuredResult;
  209. }
  210. else if (onlyFixed == row.Height.IsAbsolute)
  211. result = Math.Max(result, measurement.MeasuredResult);
  212. result = Math.Max(result, row.MinHeight);
  213. }
  214. }
  215. return result;
  216. }
  217. }
  218. private readonly AvaloniaList<MeasurementCache> _measurementCaches;
  219. private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>();
  220. public SharedSizeScopeHost(Control scope)
  221. {
  222. _measurementCaches = GetParticipatingGrids(scope);
  223. foreach (var cache in _measurementCaches)
  224. {
  225. cache.Grid.InvalidateMeasure();
  226. AddGridToScopes(cache);
  227. }
  228. }
  229. void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change)
  230. {
  231. RemoveFromGroup(change.oldName, change.result);
  232. AddToGroup(change.newName, change.result);
  233. }
  234. private bool _invalidating;
  235. internal void InvalidateMeasure(Grid grid)
  236. {
  237. // prevent stack overflow
  238. if (_invalidating)
  239. return;
  240. _invalidating = true;
  241. InvalidateMeasureImpl(grid);
  242. _invalidating = false;
  243. }
  244. private void InvalidateMeasureImpl(Grid grid)
  245. {
  246. var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
  247. if (cache == null)
  248. throw new AvaloniaInternalException(
  249. $"InvalidateMeasureImpl - called with a grid not present in the internal cache");
  250. // already invalidated the cache, early out.
  251. if (cache.MeasurementState == MeasurementState.Invalidated)
  252. return;
  253. cache.InvalidateMeasure();
  254. // maybe there is a condition to only call arrange on some of the calls?
  255. grid.InvalidateMeasure();
  256. // find all the scopes within the invalidated grid
  257. var scopeNames = cache.Results
  258. .Where(mr => mr.SizeGroup != null)
  259. .Select(mr => mr.SizeGroup.Name)
  260. .Distinct();
  261. // find all grids related to those scopes
  262. var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results)
  263. .Select(r => r.OwningGrid)
  264. .Where(g => g.IsMeasureValid)
  265. .Distinct();
  266. // invalidate them as well
  267. foreach (var otherGrid in otherGrids)
  268. {
  269. InvalidateMeasureImpl(otherGrid);
  270. }
  271. }
  272. internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
  273. {
  274. var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
  275. Debug.Assert(cache != null);
  276. cache.UpdateMeasureResult(rowResult, columnResult);
  277. }
  278. (List<GridLayout.LengthConvention>, List<double>, double) Arrange(IReadOnlyList<DefinitionBase> definitions, GridLayout.MeasureResult measureResult)
  279. {
  280. var conventions = measureResult.LeanLengthList.ToList();
  281. var lengths = measureResult.LengthList.ToList();
  282. var desiredLength = 0.0;
  283. for (int i = 0; i < definitions.Count; i++)
  284. {
  285. var definition = definitions[i];
  286. if (string.IsNullOrEmpty(definition.SharedSizeGroup))
  287. {
  288. desiredLength += measureResult.LengthList[i];
  289. continue;
  290. }
  291. var group = _groups[definition.SharedSizeGroup];
  292. var length = group.CalculatedLength;
  293. conventions[i] = new GridLayout.LengthConvention(
  294. new GridLength(length),
  295. measureResult.LeanLengthList[i].MinLength,
  296. measureResult.LeanLengthList[i].MaxLength
  297. );
  298. lengths[i] = length;
  299. desiredLength += length;
  300. }
  301. return (conventions, lengths, desiredLength);
  302. }
  303. internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
  304. {
  305. var (rowConventions, rowLengths, rowDesiredLength) = Arrange(grid.RowDefinitions, rowResult);
  306. var (columnConventions, columnLengths, columnDesiredLength) = Arrange(grid.ColumnDefinitions, columnResult);
  307. return (
  308. new GridLayout.MeasureResult(
  309. rowResult.ContainerLength,
  310. rowDesiredLength,
  311. rowResult.GreedyDesiredLength,//??
  312. rowConventions,
  313. rowLengths),
  314. new GridLayout.MeasureResult(
  315. columnResult.ContainerLength,
  316. columnDesiredLength,
  317. columnResult.GreedyDesiredLength, //??
  318. columnConventions,
  319. columnLengths)
  320. );
  321. }
  322. private void AddGridToScopes(MeasurementCache cache)
  323. {
  324. cache.GroupChanged.Subscribe(SharedGroupChanged);
  325. foreach (var result in cache.Results)
  326. {
  327. var scopeName = result.Definition.SharedSizeGroup;
  328. AddToGroup(scopeName, result);
  329. }
  330. }
  331. private void AddToGroup(string scopeName, MeasurementResult result)
  332. {
  333. if (string.IsNullOrEmpty(scopeName))
  334. return;
  335. if (!_groups.TryGetValue(scopeName, out var group))
  336. _groups.Add(scopeName, group = new Group(scopeName));
  337. group.IsFixed |= IsFixed(result.Definition);
  338. group.Add(result);
  339. }
  340. private bool IsFixed(DefinitionBase definition)
  341. {
  342. return ((definition as ColumnDefinition)?.Width ?? ((RowDefinition)definition).Height).IsAbsolute;
  343. }
  344. private void RemoveGridFromScopes(MeasurementCache cache)
  345. {
  346. foreach (var result in cache.Results)
  347. {
  348. var scopeName = result.Definition.SharedSizeGroup;
  349. RemoveFromGroup(scopeName, result);
  350. }
  351. }
  352. private void RemoveFromGroup(string scopeName, MeasurementResult result)
  353. {
  354. if (string.IsNullOrEmpty(scopeName))
  355. return;
  356. Debug.Assert(_groups.TryGetValue(scopeName, out var group));
  357. group.Remove(result);
  358. if (!group.Results.Any())
  359. _groups.Remove(scopeName);
  360. else
  361. {
  362. group.IsFixed = group.Results.Select(r => r.Definition).Any(IsFixed);
  363. }
  364. }
  365. private static AvaloniaList<MeasurementCache> GetParticipatingGrids(Control scope)
  366. {
  367. var result = scope.GetVisualDescendants().OfType<Grid>();
  368. return new AvaloniaList<MeasurementCache>(
  369. result.Where(g => g.HasSharedSizeGroups())
  370. .Select(g => new MeasurementCache(g)));
  371. }
  372. public void Dispose()
  373. {
  374. foreach (var cache in _measurementCaches)
  375. {
  376. cache.Grid.SharedScopeChanged();
  377. cache.Dispose();
  378. }
  379. }
  380. internal void RegisterGrid(Grid toAdd)
  381. {
  382. Debug.Assert(!_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd)));
  383. var cache = new MeasurementCache(toAdd);
  384. _measurementCaches.Add(cache);
  385. AddGridToScopes(cache);
  386. }
  387. internal void UnegisterGrid(Grid toRemove)
  388. {
  389. var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove));
  390. Debug.Assert(cache != null);
  391. _measurementCaches.Remove(cache);
  392. RemoveGridFromScopes(cache);
  393. cache.Dispose();
  394. }
  395. }
  396. }