RealizedStackElements.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. using System;
  2. using System.Collections.Generic;
  3. namespace Avalonia.Controls.Utils
  4. {
  5. /// <summary>
  6. /// Stores the realized element state for a virtualizing panel that arranges its children
  7. /// in a stack layout, such as <see cref="VirtualizingStackPanel"/>.
  8. /// </summary>
  9. internal class RealizedStackElements
  10. {
  11. private int _firstIndex;
  12. private List<Control?>? _elements;
  13. private List<double>? _sizes;
  14. private double _startU;
  15. private bool _startUUnstable;
  16. /// <summary>
  17. /// Gets the number of realized elements.
  18. /// </summary>
  19. public int Count => _elements?.Count ?? 0;
  20. /// <summary>
  21. /// Gets the index of the first realized element, or -1 if no elements are realized.
  22. /// </summary>
  23. public int FirstIndex => _elements?.Count > 0 ? _firstIndex : -1;
  24. /// <summary>
  25. /// Gets the index of the last realized element, or -1 if no elements are realized.
  26. /// </summary>
  27. public int LastIndex => _elements?.Count > 0 ? _firstIndex + _elements.Count - 1 : -1;
  28. /// <summary>
  29. /// Gets the elements.
  30. /// </summary>
  31. public IReadOnlyList<Control?> Elements => _elements ??= new List<Control?>();
  32. /// <summary>
  33. /// Gets the sizes of the elements on the primary axis.
  34. /// </summary>
  35. public IReadOnlyList<double> SizeU => _sizes ??= new List<double>();
  36. /// <summary>
  37. /// Gets the position of the first element on the primary axis.
  38. /// </summary>
  39. public double StartU => _startU;
  40. /// <summary>
  41. /// Adds a newly realized element to the collection.
  42. /// </summary>
  43. /// <param name="index">The index of the element.</param>
  44. /// <param name="element">The element.</param>
  45. /// <param name="u">The position of the elemnt on the primary axis.</param>
  46. /// <param name="sizeU">The size of the element on the primary axis.</param>
  47. public void Add(int index, Control element, double u, double sizeU)
  48. {
  49. if (index < 0)
  50. throw new ArgumentOutOfRangeException(nameof(index));
  51. _elements ??= new List<Control?>();
  52. _sizes ??= new List<double>();
  53. if (Count == 0)
  54. {
  55. _elements.Add(element);
  56. _sizes.Add(sizeU);
  57. _startU = u;
  58. _firstIndex = index;
  59. }
  60. else if (index == LastIndex + 1)
  61. {
  62. _elements.Add(element);
  63. _sizes.Add(sizeU);
  64. }
  65. else if (index == FirstIndex - 1)
  66. {
  67. --_firstIndex;
  68. _elements.Insert(0, element);
  69. _sizes.Insert(0, sizeU);
  70. _startU = u;
  71. }
  72. else
  73. {
  74. throw new NotSupportedException("Can only add items to the beginning or end of realized elements.");
  75. }
  76. }
  77. /// <summary>
  78. /// Gets the element at the specified index, if realized.
  79. /// </summary>
  80. /// <param name="index">The index in the source collection of the element to get.</param>
  81. /// <returns>The element if realized; otherwise null.</returns>
  82. public Control? GetElement(int index)
  83. {
  84. var i = index - FirstIndex;
  85. if (i >= 0 && i < _elements?.Count)
  86. return _elements[i];
  87. return null;
  88. }
  89. /// <summary>
  90. /// Gets the index and start U position of the element at the specified U position.
  91. /// </summary>
  92. /// <param name="u">The U position.</param>
  93. /// <returns>
  94. /// A tuple containing:
  95. /// - The index of the item at the specified U position, or -1 if the item could not be
  96. /// determined
  97. /// - The U position of the start of the item, if determined
  98. /// </returns>
  99. public (int index, double position) GetIndexAt(double u)
  100. {
  101. if (_elements is null || _sizes is null || _startU > u || _startUUnstable)
  102. return (-1, 0);
  103. var index = 0;
  104. var position = _startU;
  105. while (index < _elements.Count)
  106. {
  107. var size = _sizes[index];
  108. if (double.IsNaN(size))
  109. break;
  110. if (u >= position && u < position + size)
  111. return (index + FirstIndex, position);
  112. position += size;
  113. ++index;
  114. }
  115. return (-1, 0);
  116. }
  117. /// <summary>
  118. /// Gets the element at the specified position on the primary axis, if realized.
  119. /// </summary>
  120. /// <param name="position">The position.</param>
  121. /// <returns>
  122. /// A tuple containing the index of the element (or -1 if not found) and the position of the element on the
  123. /// primary axis.
  124. /// </returns>
  125. public (int index, double position) GetElementAt(double position)
  126. {
  127. if (_sizes is null || position < StartU)
  128. return (-1, 0);
  129. var u = StartU;
  130. var i = FirstIndex;
  131. foreach (var size in _sizes)
  132. {
  133. var endU = u + size;
  134. if (position < endU)
  135. return (i, u);
  136. u += size;
  137. ++i;
  138. }
  139. return (-1, 0);
  140. }
  141. /// <summary>
  142. /// Estimates the average U size of all elements in the source collection based on the
  143. /// realized elements.
  144. /// </summary>
  145. /// <returns>
  146. /// The estimated U size of an element, or -1 if not enough information is present to make
  147. /// an estimate.
  148. /// </returns>
  149. public double EstimateElementSizeU()
  150. {
  151. var total = 0.0;
  152. var divisor = 0.0;
  153. // Start by averaging the size of the elements before the first realized element.
  154. if (FirstIndex >= 0 && !_startUUnstable)
  155. {
  156. total += _startU;
  157. divisor += FirstIndex;
  158. }
  159. // Average the size of the realized elements.
  160. if (_sizes is not null)
  161. {
  162. foreach (var size in _sizes)
  163. {
  164. if (double.IsNaN(size))
  165. continue;
  166. total += size;
  167. ++divisor;
  168. }
  169. }
  170. // We don't have any elements on which to base our estimate.
  171. if (divisor == 0 || total == 0)
  172. return -1;
  173. return total / divisor;
  174. }
  175. /// <summary>
  176. /// Gets the index of the specified element.
  177. /// </summary>
  178. /// <param name="element">The element.</param>
  179. /// <returns>The index or -1 if the element is not present in the collection.</returns>
  180. public int GetIndex(Control element)
  181. {
  182. return _elements?.IndexOf(element) is int index && index >= 0 ? index + FirstIndex : -1;
  183. }
  184. /// <summary>
  185. /// Updates the elements in response to items being inserted into the source collection.
  186. /// </summary>
  187. /// <param name="index">The index in the source collection of the insert.</param>
  188. /// <param name="count">The number of items inserted.</param>
  189. /// <param name="updateElementIndex">A method used to update the element indexes.</param>
  190. public void ItemsInserted(int index, int count, Action<Control, int, int> updateElementIndex)
  191. {
  192. if (index < 0)
  193. throw new ArgumentOutOfRangeException(nameof(index));
  194. if (_elements is null || _elements.Count == 0)
  195. return;
  196. // Get the index within the realized _elements collection.
  197. var first = FirstIndex;
  198. var realizedIndex = index - first;
  199. if (realizedIndex < Count)
  200. {
  201. // The insertion point affects the realized elements. Update the index of the
  202. // elements after the insertion point.
  203. var elementCount = _elements.Count;
  204. var start = Math.Max(realizedIndex, 0);
  205. var newIndex = realizedIndex + count;
  206. for (var i = start; i < elementCount; ++i)
  207. {
  208. if (_elements[i] is Control element)
  209. updateElementIndex(element, newIndex - count, newIndex);
  210. ++newIndex;
  211. }
  212. if (realizedIndex < 0)
  213. {
  214. // The insertion point was before the first element, update the first index.
  215. _firstIndex += count;
  216. }
  217. else
  218. {
  219. // The insertion point was within the realized elements, insert an empty space
  220. // in _elements and _sizes.
  221. _elements!.InsertMany(realizedIndex, null, count);
  222. _sizes!.InsertMany(realizedIndex, double.NaN, count);
  223. }
  224. }
  225. }
  226. /// <summary>
  227. /// Updates the elements in response to items being removed from the source collection.
  228. /// </summary>
  229. /// <param name="index">The index in the source collection of the remove.</param>
  230. /// <param name="count">The number of items removed.</param>
  231. /// <param name="updateElementIndex">A method used to update the element indexes.</param>
  232. /// <param name="recycleElement">A method used to recycle elements.</param>
  233. public void ItemsRemoved(
  234. int index,
  235. int count,
  236. Action<Control, int, int> updateElementIndex,
  237. Action<Control> recycleElement)
  238. {
  239. if (index < 0)
  240. throw new ArgumentOutOfRangeException(nameof(index));
  241. if (_elements is null || _elements.Count == 0)
  242. return;
  243. // Get the removal start and end index within the realized _elements collection.
  244. var first = FirstIndex;
  245. var last = LastIndex;
  246. var startIndex = index - first;
  247. var endIndex = (index + count) - first;
  248. if (endIndex < 0)
  249. {
  250. // The removed range was before the realized elements. Update the first index and
  251. // the indexes of the realized elements.
  252. _firstIndex -= count;
  253. _startUUnstable = true;
  254. var newIndex = _firstIndex;
  255. for (var i = 0; i < _elements.Count; ++i)
  256. {
  257. if (_elements[i] is Control element)
  258. updateElementIndex(element, newIndex - count, newIndex);
  259. ++newIndex;
  260. }
  261. }
  262. else if (startIndex < _elements.Count)
  263. {
  264. // Recycle and remove the affected elements.
  265. var start = Math.Max(startIndex, 0);
  266. var end = Math.Min(endIndex, _elements.Count);
  267. for (var i = start; i < end; ++i)
  268. {
  269. if (_elements[i] is Control element)
  270. recycleElement(element);
  271. }
  272. _elements.RemoveRange(start, end - start);
  273. _sizes!.RemoveRange(start, end - start);
  274. // If the remove started before and ended within our realized elements, then our new
  275. // first index will be the index where the remove started. Mark StartU as unstable
  276. // because we can't rely on it now to estimate element heights.
  277. if (startIndex <= 0 && end < last)
  278. {
  279. _firstIndex = first = index;
  280. _startUUnstable = true;
  281. }
  282. // Update the indexes of the elements after the removed range.
  283. end = _elements.Count;
  284. var newIndex = first + start;
  285. for (var i = start; i < end; ++i)
  286. {
  287. if (_elements[i] is Control element)
  288. updateElementIndex(element, newIndex + count, newIndex);
  289. ++newIndex;
  290. }
  291. }
  292. }
  293. /// <summary>
  294. /// Recycles all elements in response to the source collection being reset.
  295. /// </summary>
  296. /// <param name="recycleElement">A method used to recycle elements.</param>
  297. public void ItemsReset(Action<Control> recycleElement)
  298. {
  299. if (_elements is null || _elements.Count == 0)
  300. return;
  301. foreach (var e in _elements)
  302. {
  303. if (e is not null)
  304. recycleElement(e);
  305. }
  306. _startU = _firstIndex = 0;
  307. _elements?.Clear();
  308. _sizes?.Clear();
  309. }
  310. /// <summary>
  311. /// Recycles elements before a specific index.
  312. /// </summary>
  313. /// <param name="index">The index in the source collection of new first element.</param>
  314. /// <param name="recycleElement">A method used to recycle elements.</param>
  315. public void RecycleElementsBefore(int index, Action<Control, int> recycleElement)
  316. {
  317. if (index <= FirstIndex || _elements is null || _elements.Count == 0)
  318. return;
  319. if (index > LastIndex)
  320. {
  321. RecycleAllElements(recycleElement);
  322. }
  323. else
  324. {
  325. var endIndex = index - FirstIndex;
  326. for (var i = 0; i < endIndex; ++i)
  327. {
  328. if (_elements[i] is Control e)
  329. recycleElement(e, i + FirstIndex);
  330. }
  331. _elements.RemoveRange(0, endIndex);
  332. _sizes!.RemoveRange(0, endIndex);
  333. _firstIndex = index;
  334. }
  335. }
  336. /// <summary>
  337. /// Recycles elements after a specific index.
  338. /// </summary>
  339. /// <param name="index">The index in the source collection of new last element.</param>
  340. /// <param name="recycleElement">A method used to recycle elements.</param>
  341. public void RecycleElementsAfter(int index, Action<Control, int> recycleElement)
  342. {
  343. if (index >= LastIndex || _elements is null || _elements.Count == 0)
  344. return;
  345. if (index < FirstIndex)
  346. {
  347. RecycleAllElements(recycleElement);
  348. }
  349. else
  350. {
  351. var startIndex = (index + 1) - FirstIndex;
  352. var count = _elements.Count;
  353. for (var i = startIndex; i < count; ++i)
  354. {
  355. if (_elements[i] is Control e)
  356. recycleElement(e, i + FirstIndex);
  357. }
  358. _elements.RemoveRange(startIndex, _elements.Count - startIndex);
  359. _sizes!.RemoveRange(startIndex, _sizes.Count - startIndex);
  360. }
  361. }
  362. /// <summary>
  363. /// Recycles all realized elements.
  364. /// </summary>
  365. /// <param name="recycleElement">A method used to recycle elements.</param>
  366. public void RecycleAllElements(Action<Control, int> recycleElement)
  367. {
  368. if (_elements is null || _elements.Count == 0)
  369. return;
  370. var i = FirstIndex;
  371. foreach (var e in _elements)
  372. {
  373. if (e is not null)
  374. recycleElement(e, i);
  375. ++i;
  376. }
  377. _startU = _firstIndex = 0;
  378. _elements?.Clear();
  379. _sizes?.Clear();
  380. }
  381. /// <summary>
  382. /// Resets the element list and prepares it for reuse.
  383. /// </summary>
  384. public void ResetForReuse()
  385. {
  386. _startU = _firstIndex = 0;
  387. _startUUnstable = false;
  388. _elements?.Clear();
  389. _sizes?.Clear();
  390. }
  391. }
  392. }